UIDex

مجموعة أزرار الاختيار

Radio Group

role="radiogroup"

اسمه كمانراديو بتن، أزرار الراديو، أزرار الاختيار، مجموعة الخيارات، اختيار واحد من متعدّد، راديو جروب، radio buttons، radiogroup، option buttons، single choice group، radio list، mutually exclusive options

مجموعة أزرار الاختيار عنصر واحد يعرض بدائل يستبعد بعضها بعضاً، فلا تُختار منها إلا قيمة واحدة في المرّة. والزرّ المفرد داخلها لا معنى له وحده: ما يجعل الخيارات متنافية هو انتماؤها إلى مجموعة واحدة، لا شكل الدائرة. وهي ليست مربّعات اختيار (checkboxes)، لأن كل مربّع قيمة منطقية مستقلّة تُقرأ وحدها، والمستخدم فيها يعلّم ما شاء أو لا يعلّم شيئاً. وهي ليست قائمة منسدلة، فالقائمة تؤدّي نفس الوظيفة في مساحة أصغر وتخفي البدائل خلف نقرة، أمّا المجموعة فتضع البدائل كلها أمام العين ليقارن بينها المستخدم قبل أن يقرّر، ولهذا تصلح حتى نحو سبعة خيارات وتُترك القائمة لما زاد على ذلك. وهي ليست عنصر التقسيم (segmented control)، فذاك شكل مضغوط لنفس الاختيار الواحد يجمع البدائل في شريط ملتحم، ويُستعمل غالباً لتبديل عرض قائم أمام المستخدم في الحال. ويحسم النقاش في المراجعة اختباران: إذا جاز أن يصحّ جوابان معاً فهذه ليست مجموعة اختيار، وإذا كان «لا شيء» جواباً مقبولاً فالمجموعة تحتاج خيار «بدون» صريحاً أو زرّ مسح، لأن الضغط مرّة ثانية على الخيار المختار لا يلغيه.

لو قلت عليه…

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

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

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

طريقة الشحن

القيمة المرسلة مع النموذج: shipping=express

RTLالأسهم تنقل وتختار معاً، و ArrowLeft هو «التالي» في العربية

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

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

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

ابنِ مجموعة أزرار اختيار لسؤال واحد. لُفّ الخيارات في <fieldset> واجعل <legend> أوّل عنصر فيه يحمل نصّ السؤال، وإن كتبت role="radiogroup" فوق الـ fieldset فأضف aria-labelledby يشير إلى id الـ legend. اجعل كل خيار <input type="radio"> حقيقياً يشترك مع إخوته في نفس قيمة name وينفرد بقيمة value، واربط به <label for="…"> بحيث يختار الضغط على النصّ نفسه، وبحشو يجعل صفّ الخيار كلّه هدف لمس لا يقلّ عن ٤٤ بكسل. اترك لوحة المفاتيح للمتصفّح: المجموعة الأصلية محطّة Tab واحدة بالفعل، وأسهمها تنقل وتختار في نفس الحركة مع الالتفاف، فلا تضع tabindex على الـ inputs. وإن استبدلت بها عناصر بـ role="radio" فأعد بناء ذلك بنفسك: tabindex="0" على الخيار المختار و‎-1 على الباقي، وأوّل خيار في الـ DOM هو المحطّة عند غياب اختيار مسبق، و ArrowUp و ArrowDown ثابتا المعنى، أمّا ArrowLeft و ArrowRight فيتبادلان عندما يكون اتجاه المجموعة rtl فاقرأه وقت التنفيذ من group.matches(":dir(rtl)") لا من لغة الواجهة. لا تُعلن الاختيار باللون وحده بل بنقطة ممتلئة داخل الدائرة، وارسم حلقة التركيز بـ :focus-visible على الخيار المركَّز لا على المجموعة. وإن كان «لا شيء» جواباً مقبولاً فأضف خيار «بدون» صريحاً أو زرّ مسح، لأن المستخدم لا يلغي اختياره بالضغط مرّة ثانية. استعمل الخصائص المنطقية في كل مكان: gap و padding-inline و margin-inline-start و inset-inline-start، ولا تكتب left ولا right ولا margin-left. ولو أضفت مؤشّراً ينزلق خلف الخيار المختار فاحسب موضعه من البداية المنطقية بـ getBoundingClientRect لا بـ offsetLeft، وحرّكه بـ inset-inline-start أو بـ translateX مضروباً في متغيّر ‎--dir. وإن زادت الخيارات على سبعة فانقل السؤال إلى قائمة منسدلة.

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

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

معنى سهمَي اليمين واليسار

ينعكس

في مجموعة أفقية ينقلب معنى السهمين مع الاتجاه: ArrowRight هو «التالي» في LTR، وفي RTL يصير ArrowLeft هو التالي و ArrowRight هو السابق، تماماً كما في شريط التبويبات. Chromium يطبّق هذا وحده على <input type="radio"> الأصلي، فتحت direction: rtl ينقل ArrowLeft التركيز إلى الخيار التالي في ترتيب المصدر ويختاره معه، أمّا مجموعة يدوية بـ role="radio" فلا ينالها شيء من ذلك: اقرأ الاتجاه وقت التنفيذ من group.matches(":dir(rtl)") أو getComputedStyle(group).direction ثم اضرب الخطوة في ‎-1، ولا تستنتجه من لغة الواجهة لأن أي حاوية قد تُقلب وحدها. أمّا ArrowUp و ArrowDown فلا ينقلبان أبداً، لأن الانعكاس يقع على المحور المضمّن وحده والمحور الكتليّ يبقى من أعلى إلى أسفل في اللغتين.

موضع الدائرة من نصّها

ينعكس

الدائرة تجلس عند البداية المنطقية للخيار، أي يميناً في العربية ويساراً في الإنجليزية: اجعل صفّ الخيار flex بفجوة gap ودع ترتيب الـ DOM يتكفّل بالباقي، أو استعمل padding-inline-start بدل padding-left إن رسمت الدائرة بـ ::before. والنصّ الوصفيّ تحت التسمية يُزاح بـ margin-inline-start حتى يحاذي أوّل حرف من التسمية لا حافّة الدائرة، وأيّ margin-left هنا يدفعه إلى الجهة الخطأ بمجرّد انعكاس الصفحة. أمّا إخفاء الـ input الأصلي بـ position: absolute; left: -9999px فلا يكلّف شيئاً في LTR ويضيف شريط تمرير أفقياً في RTL: القياس في Chrome يُظهر أن input واحداً يرفع scrollWidth لحاوية عرضها ٣٠٠ بكسل إلى نحو ١٠٠٠٠ بكسل، لأن الفائض نحو اليسار قابل للتمرير تحت direction: rtl ويُقصّ تحت ltr، فاقصص الـ input في مكانه بمربّع ١ بكسل مع clip-path: inset(50%).

التفاف الصفّ الأفقي

ينعكس

الصفّ الأفقي يُملأ من اليمين في العربية، وعند الالتفاف يبدأ السطر الثاني من اليمين أيضاً، وهذا هو سلوك flex-wrap الافتراضي تحت direction: rtl فلا يحتاج أيّ تدخّل. لا تكتب flex-direction: row-reverse لتصحيح الاتجاه، فالمحور المضمّن انعكس مرّة بالفعل، والعكس الثاني يُخالف بين الترتيب البصري وترتيب الـ DOM فتتحرّك الأسهم عكس ما تراه العين. ولو بنيت التخطيط بـ grid فترقيم خطوط الشبكة يتبع المحور المضمّن كذلك: grid-column: 1 هو العمود الأيمن في العربية، فاستعمل الترقيم كما يأتي ولا تعكسه يدوياً.

انتقال المؤشّر المنزلق

ينعكس

إذا رسمت المجموعة شريطاً ملتحماً بمؤشّر ينزلق خلف الخيار المختار، فالمؤشّر يسافر على المحور المنطقي من اليمين إلى اليسار في العربية، و offsetLeft عاجز عن وصف ذلك لأنه يُقاس من الحافّة اليسرى مهما كان الاتجاه فيوقف المؤشّر تحت الخيار الخطأ. قِس المسافة من البداية المنطقية بدلاً منه: في RTL هي group.getBoundingClientRect().right ناقص item.right، وفي LTR هي item.left ناقص group.left. ثمّ حرّكه بـ inset-inline-start، أو بـ translateX مضروباً في متغيّر ‎--dir قيمته ‎-1 داخل [dir="rtl"]، لأن transform فيزيائي ولا مكافئ منطقي له في CSS.

ما يبقى على ترتيب الـ DOM

لا ينعكس

الانعكاس بصريّ بحت ولا يمسّ ترتيب الـ DOM، فـ«الخيار الأوّل» يظلّ أوّل عنصر في المصدر وهو الأقصى يميناً في مجموعة أفقية عربية. وعليه فنقطة الدخول عند غياب اختيار مسبق هي هذا العنصر نفسه، والالتفاف بالأسهم يتبع ترتيب المصدر فيقفز التركيز من الخيار الأقصى يساراً على الشاشة إلى الأقصى يميناً، وإن أضفت Home و End فهما يقصدان أوّل وآخر عنصر في المصدر كذلك. كما أن name و value بيانات لا تخطيط فلا تتغيّران مع الاتجاه، وترتيب الحقول في FormData يتبع المصدر لا الشاشة، والدائرة والنقطة داخلها متماثلتان حول محورهما الرأسي فأخرجهما من أيّ قاعدة scaleX(-1) عامّة تطبّقها على الأيقونات.

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

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

ARIArole="radiogroup" + role="radio"الدور الذي يجعل البدائل عنصراً واحداً أمام قارئ الشاشة، ومنه يأتي العدّ «٢ من ٤».
HTML<input type="radio" name="shipping">قيمة name المشتركة هي آلية التجميع نفسها، ونطاقها النموذج كلّه لا الـ fieldset.
HTML<fieldset> + <legend>الاسم المقروء للمجموعة، بشرط أن يكون <legend> أوّل عنصر داخل <fieldset>.
Radix UI<RadioGroup.Root dir="rtl" orientation="horizontal">يتكفّل بالترحيل بين الخيارات وبقلب الأسهم، و dir يُضبط هنا على مستوى المكوّن لا الصفحة.
Material UI<RadioGroup row aria-labelledby="…">الخاصية row تصفّ الخيارات أفقياً، و FormControlLabel يربط كل Radio بنصّه، أمّا اسم المجموعة فلا يأتي إلا من aria-labelledby تشير به إلى id الـ FormLabel، فوجودهما داخل FormControl لا يربطهما.
Ant Design<Radio.Group optionType="button">نفس المجموعة مرسومة شريطاً ملتحماً، وهي أقرب عندئذ إلى عنصر التقسيم منها إلى الدوائر، والخاصية لا تعمل إلا مع مصفوفة options.
CSSaccent-colorسطر واحد يلوّن الدائرة الأصلية بلون علامتك، ويغنيك عن إخفاء الـ input وإعادة بنائه.

شوف كمان

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