UIDex

Radio Group

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

role="radiogroup"

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

A radio group is one control holding options that rule each other out, so at most one of them is selected at a time. A single radio means nothing on its own: what makes the options exclusive is belonging to one group, not the round shape. It is not a set of checkboxes, since each checkbox is an independent boolean read by itself, and the user can tick as many or as few of them as they want. It is not a select either. A select does the same job in less room by hiding the options behind a click, while a radio group puts every option in front of the eye so they can be compared before one is picked, which is why it suits roughly seven options and a select takes over above that. It is also not a segmented control, which is a compact shape for the same single choice, joining the options into one strip, usually to switch a view the user is already looking at. Two checks settle it in review: if two answers can be true at once this is not a radio group, and if "none" is a legitimate answer the group needs an explicit None option or a Clear button, because a second press on the chosen radio does not clear it.

If you called it…

"the circles where you can only pick one""buttons that unselect each other""checkboxes but round and only one at a time""the dots for choosing a delivery speed""the options I cannot untick again"

Live specimen

Interact with the demo. Every part is real and numbered.

طريقة الشحن

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

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

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a radio group for one question. Wrap the options in a <fieldset> whose first child is a <legend> carrying the question text, and if you put role="radiogroup" on the fieldset, add an aria-labelledby pointing at the legend id. Make every option a real <input type="radio"> that shares one name value with its siblings and owns a distinct value, tie it to a <label for="…"> so pressing the words selects it, and pad the row until the whole option is a touch target of at least 44px. Leave the keyboard to the browser: a native group is one Tab stop already and its arrows move and select in the same motion with wraparound, so put no tabindex on the inputs. If you swap them for role="radio" elements, rebuild that yourself, with tabindex="0" on the selected option and -1 on the rest, the first option in DOM order holding the stop while nothing is selected, ArrowUp and ArrowDown keeping their meaning, and ArrowLeft and ArrowRight swapping once the group direction is rtl, read at runtime from group.matches(":dir(rtl)") rather than from the UI language. Do not announce the choice by colour alone: put a filled dot inside the circle, and draw the focus ring with :focus-visible on the focused option instead of on the group. If "nothing" is an acceptable answer, add an explicit None option or a Clear button, because pressing again does not undo a radio. Use logical properties everywhere: gap, padding-inline, margin-inline-start, inset-inline-start, and write no left, no right, and no margin-left. If you add an indicator that slides behind the selected option, compute its offset from the logical start with getBoundingClientRect instead of offsetLeft, and move it with inset-inline-start or with translateX multiplied by a --dir variable. Above seven options, move the question to a select.

How it behaves right-to-left

This section is ours alone.

What the left and right arrows mean

mirrors

In a horizontal group the two arrows swap meaning with direction: ArrowRight is "next" in LTR, while in RTL ArrowLeft becomes next and ArrowRight becomes previous, exactly as in a tab strip. Chromium already applies that to a native <input type="radio">, where ArrowLeft under direction: rtl moves to the next input in source order and selects it, while a group hand-built from role="radio" inherits none of it: read the direction at runtime with group.matches(":dir(rtl)") or getComputedStyle(group).direction, then multiply the step by -1 rather than inferring it from the UI language, since one container can be flipped on its own. ArrowUp and ArrowDown never swap, because only the inline axis mirrors and the block axis runs top to bottom in both languages.

Where the circle sits

mirrors

The circle sits at the logical start of the option, on the right in Arabic and on the left in English: make the option row a flex container with gap and let DOM order do the rest, or use padding-inline-start instead of padding-left when the circle is drawn with ::before. Help text under the label is indented with margin-inline-start so it lines up with the first letter of the label rather than the edge of the circle, and a margin-left here pushes it to the wrong side the moment the page flips. Hiding the real input with position: absolute; left: -9999px is free in LTR and adds a horizontal scrollbar in RTL: measured in Chrome, one such input takes a 300px scroll container from a 300px scroll width to about 10,000px, because leftward overflow is scrollable under direction: rtl and clipped under ltr, so clip the input in place with a 1px box and clip-path: inset(50%).

How a horizontal group wraps

mirrors

A horizontal group fills from the right in Arabic and starts its second line from the right on wrap, which is what flex-wrap already does under direction: rtl. Do not write flex-direction: row-reverse to correct the direction: the inline axis has flipped once already, and the second flip leaves visual order disagreeing with DOM order, so the arrow keys move against what the eye sees. Grid follows the same inline axis, so grid-column: 1 is the rightmost column in Arabic, and the numbering should be used as it comes instead of reversed by hand.

The travelling indicator

mirrors

When the group is drawn as a joined strip with an indicator sliding behind the selected option, that indicator travels along the logical axis, right to left in Arabic, and offsetLeft cannot express it because it measures from the left edge whatever the direction. Measure from the logical start instead: in RTL that is group.getBoundingClientRect().right minus item.right, and in LTR it is item.left minus group.left. Then move it with inset-inline-start, or with translateX multiplied by a --dir variable set to -1 under [dir="rtl"], because transform is physical and CSS gives it no logical counterpart.

What stays in DOM order

never mirrors

The flip is visual only and does not touch DOM order, so "the first option" is still the first element in the source, which in a horizontal Arabic group is the rightmost one. The entry point when nothing is selected yet is that same element, arrow wraparound follows source order and therefore throws focus from the option at the visual far left back to the one at the far right, and Home and End mean first and last in source order too. name and value are data rather than layout, so they do not change with direction, field order in FormData follows the source instead of the screen, and the circle with its dot is symmetrical about its vertical axis, which keeps it out of any blanket scaleX(-1) rule you apply to icons.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIArole="radiogroup" + role="radio"The role that turns the options into one control for a screen reader, and the source of the "2 of 4" count.
HTML<input type="radio" name="shipping">The shared name value is the grouping mechanism itself, and its scope is the whole form, not the fieldset.
HTML<fieldset> + <legend>The accessible name of the group, valid only while <legend> is the first child of <fieldset>.
Radix UI<RadioGroup.Root dir="rtl" orientation="horizontal">Handles the roving focus and the arrow inversion for you, with dir set per component instead of per page.
Material UI<RadioGroup row aria-labelledby="…">The row prop lays the options out horizontally and FormControlLabel pairs each Radio with its text, but the group is named only by an aria-labelledby you point at a FormLabel id, which sitting inside the same FormControl will not do for you.
Ant Design<Radio.Group optionType="button">The same group drawn as a joined strip, closer to a segmented control than to circles, and the prop only takes effect when you pass the options array.
CSSaccent-colorOne line paints the native circle in your brand colour and saves you from hiding the input to rebuild it.

See also

Updated · 2026-08-19