UIDex

مفتاح التبديل

Switch / Toggle

role="switch"

اسمه كمانسويتش، توجل، مفتاح تشغيل وإيقاف، مفتاح الإعدادات، زرّ التفعيل، مفتاح منزلق، toggle، toggle switch، on/off switch، settings toggle، switch control، slider toggle

مفتاح التبديل عنصر تحكّم له حالتان فقط: مُفعّل أو مُعطّل. وهو ينفّذ التغيير في اللحظة التي يضغطه فيها المستخدم، بلا زرّ حفظ ولا خطوة تأكيد. مكانه الطبيعي شاشات الإعدادات وكل خيار يسري أثره فوراً: الوضع الداكن، الإشعارات، المزامنة في الخلفية. وهو ليس مربّع اختيار (checkbox)، فمربّع الاختيار قيمة تُجمَع مع بقية النموذج وتُرسَل عند الضغط على «حفظ»، ويظلّ أمام المستخدم متّسع للتراجع قبل الإرسال، أمّا المفتاح فلا تراجع فيه لأن الأثر وقع قبل أن يرفع إصبعه. وليس هو زرّ تبديل (toggle button)، فذاك زرّ يحمل aria-pressed ويصف حالة أداة داخل شريط أدوات، كزرّ «عريض» في المحرّر، بينما يحمل المفتاح role="switch" مع aria-checked ويصف إعداداً قائماً بذاته. ولا هو زوج أزرار راديو، فأزرار الراديو تعرض عدّة بدائل مسمّاة يُختار منها واحد، أمّا المفتاح فليس أمامه إلا الشيء ونقيضه. وفي المراجعة تكفي قاعدة واحدة لحسم النقاش: إذا احتاجت شاشتك زرّ «تطبيق» بجانب المفتاح فأنت لا تريد مفتاحاً أصلاً، بل مربّع اختيار داخل نموذج.

لو قلت عليه…

«الزرار اللي بيتزحلق يمين وشمال»«المفتاح بتاع الإعدادات اللي بينوّر أخضر لمّا تشغّله»«حاجة زي مفتاح النور جوّه التطبيق»«الزرار اللي بيقفل الإشعارات على طول من غير حفظ»«الكبسولة الصغيّرة اللي جوّاها دايرة بتتحرّك»

العيّنة الحيّة

تفاعل مع الديمو. كل الأجزاء حقيقية ومرقّمة.

الإعدادات

كل مفتاح يُطبَّق لحظة الضغط، ولا يوجد زرّ «حفظ» في هذه الشاشة.

RTLالقرص يسافر نحو النهاية المنطقية، فـ«مُفعّل» عند اليسار في العربية

تشريح العنصر: كل جزء واسمه

مرّر على أي سطر ليتحدّد مكانه في الديمو فوق.

برومبت البناء

ابنِ مفتاح تبديل لصفّ إعدادات. اجعل العنصر التفاعلي زرّاً حقيقياً يحمل role="switch" و aria-checked يعكس القيمة الحقيقية المطبَّقة، ولا تستعمل <div> مع onClick أبداً. اربط التسمية بـ for/id أو ضع النصّ والمسار داخل الزرّ نفسه حتى يعمل الضغط على الكلمة، واحذف الحالة من الاسم المقروء لأن aria-checked يعلنها. طبِّق التغيير فور الضغط: لا زرّ «تطبيق» ولا «حفظ» في الشاشة إطلاقاً. ولو كان الإجراء بطيئاً فاترك القرص حيث وضعه المستخدم وأضف aria-busy="true" ومؤشّر تحميل، ولا تُرجع القرص عند الفشل إلا مع رسالة خطأ صريحة. ولا تعتمد على اللون وحده: أضف رمزين داخل المسار بـ aria-hidden أو نصّ حالة مرئياً بجانب المفتاح. واضمن مساحة لمس ٢٤×٢٤ بكسل على الأقل بالحشو لا بتكبير الكبسولة. واستخدم الخصائص المنطقية في كل مكان: inset-inline-start و margin-inline و padding-inline، ولا تكتب left أو right أو margin-left. حرّك القرص بـ transform: translateX(calc(var(--travel) * var(--dir))) مع ضبط ‎--dir: -1 داخل [dir="rtl"] حتى تصل حالة «مُفعّل» إلى يسار المسار في العربية، واجعل امتلاء المسار بتحريك inline-size لا بـ scaleX، ولا تقلب علامة الصح ولا المؤشّر الدوّار عند الانعكاس.

سلوكه في الاتجاه من اليمين لليسار

الجزء ده حصري عندنا.

اتجاه سفر القرص

ينعكس

القرص يسافر دائماً نحو النهاية المنطقية للمسار، ومعنى ذلك أن حالة «مُفعّل» تجلس عند اليسار في العربية بعد أن كانت عند اليمين في الإنجليزية. والمشكلة أن transform خاصّية فيزيائية بحتة لا تعرف الاتجاه ولا يوجد لها مكافئ منطقي في CSS: translateX(22px) سيدفع القرص خارج المسار في RTL. الحلّ الصحيح translateX(calc(22px * var(--dir))) مع ضبط ‎--dir: -1 داخل [dir="rtl"]. ويمكن الاستغناء عن الإزاحة أصلاً بجعل المسار flex وتبديل justify-content بين flex-start و flex-end، لأن المحور المضمّن ينعكس مع direction من تلقاء نفسه. غير أن هذا الشكل لا يقبل التحريك، فاختر متغيّر الاتجاه إن أردت انتقالاً ناعماً.

امتلاء المسار

ينعكس

المسار الذي يمتلئ باللون خلف القرص يجب أن ينمو من البداية المنطقية، أي من اليمين في العربية. نفّذه بعنصر داخلي مثبّت بـ inset-inline-start: 0 يتحرّك inline-size فيه من صفر إلى ١٠٠٪، فينعكس من تلقاء نفسه لأن الحافّة التي رُسِي عليها هي نفسها التي تبدّلت. أمّا transform: scaleX() فلا يصلح هنا، لأن transform-origin لا تقبل أي كلمة منطقية: ليس في محورها الأفقي إلا left و right و center أو طول أو نسبة مئوية، فتضطرّ إلى كتابة transform-origin: right بيدك داخل [dir="rtl"]. ونفس القاعدة على المسار ثنائيّ اللون المرسوم بـ linear-gradient(to right, …): لا كلمات منطقية في التدرّجات، فاقلبه إلى to left عند الانعكاس.

موضع التسمية في الصفّ

ينعكس

في صفّ الإعدادات تجلس التسمية عند البداية المنطقية والمفتاح عند النهاية، فينقلب الصفّ كاملاً في العربية: النصّ يميناً والمفتاح يساراً. استعمل صفّ flex مع justify-content: space-between أو margin-inline-start: auto على المفتاح، ولا تكتب float: right ولا margin-left: auto. ولا تضف flex-direction: row-reverse داخل [dir="rtl"] ظنّاً أنك تصحّح الاتجاه: المحور المضمّن انعكس مرّة بالفعل، والعكس الثاني يعيد المفتاح إلى الجهة الخطأ ويجعل الترتيب البصري مخالفاً لترتيب DOM وللتنقّل بـ Tab.

الرمزان داخل المسار

لا ينعكس

موضعا الرمزين يتبادلان مع المسار تلقائياً لأنهما يتبعان حافّتيه المنطقيتين، لكن الرمزين نفسيهما لا ينقلبان: خاصّية Bidi_Mirrored للمحرف ✓ قيمتها No، ومعناه لا يتغيّر باختلاف اتجاه الصفحة. لذلك فالقاعدة الشاملة [dir="rtl"] svg { transform: scaleX(-1) } تكسر علامة الصح وتكسر معها مؤشّر التحميل الدوّار داخل القرص وتجعلهما يبدوان مرسومين بالمقلوب. اقصر القلب على صنف مخصّص تضعه على الأسهم وحدها.

السحب بالإصبع

ينعكس

لو سمحت بتبديل المفتاح سحباً لا نقراً، فالسحب نحو اليسار في العربية هو ما يعني «شغّل». وأحداث المؤشّر تعطيك إزاحة فيزيائية بحتة: قيمة movementX (أو الفرق بين clientX الحالي وبداية السحب) موجبة دائماً عند التحرّك يميناً مهما كان اتجاه الصفحة، فمقارنة dx > threshold تعكس معنى الإيماءة كلها في العربية. اضرب الإزاحة في إشارة اتجاه تقرأها من getComputedStyle(el).direction === "rtl" ? -1 : 1 قبل المقارنة، وأضف touch-action: pan-y على المسار حتى لا يبتلع تمرير الصفحة الإيماءة.

أسماؤه في الكود

كل سطر هو كلمة مكتبة واحدة عن نفس الشيء. خد السطر اللي بيكلّم مشروعك.

ARIArole="switch" + aria-checkedالزوج الذي يعرّف العنصر؛ لا يقبل mixed، فقط true أو false.
HTML<input type="checkbox" switch>السمة الأصلية بدأت من Safari ودعم المتصفّحات لها ما زال متفاوتاً؛ تحقّق منه قبل الاعتماد عليها وإلا فابقَ على role="switch".
Radix UI<Switch.Root> + <Switch.Thumb>يعطيك زرّاً حقيقياً بـ role="switch"، و onCheckedChange يسلّمك القيمة الجديدة لا الحدث؛ وعليه يقوم غلاف shadcn/ui.
Material UI<FormControlLabel control={<Switch />}>الغلاف هو ما يجعل النصّ نفسه قابلاً للضغط؛ <Switch /> وحده يترك التسمية معلّقة.
Ant Design<Switch loading checked />حالة الانتظار جاهزة فيه، وهي بالضبط الحالة التي ينساها التنفيذ اليدوي.
SwiftUIToggle("…", isOn: $isOn)أبل تسمّيه Toggle لا Switch، وشكل المفتاح يأتي من ‎.toggleStyle(.switch)‎.
CSStranslateX(calc(var(--travel) * var(--dir)))لا يوجد مكافئ منطقي لـ transform، فمتغيّر الاتجاه هو ما يعكس سفر القرص.

شوف كمان

آخر تحديث · 2026-08-19