Skip to content
ShineUI
Install

RadioButton

One of a few, spelled out.

<sui-radio-button><sui-radio-group>SelectionStable

About

Single radio: an 18px circle that fills navy when checked, with a label and an optional description. Radios that share a name behave as one native group. Use sui-radio-group for the set; it owns the label, help and error line, because a lone radio can be checked but never unchecked. Form-associated: posts name=value while checked.

Usage

<sui-radio-button label="Report basis" name="basis" value="Accrual"></sui-radio-button> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-radio-button"); el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiRadioButton } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiRadioButton label="Report basis" name="basis" value="Accrual" onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiRadioButton } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiRadioButton label="Report basis" name="basis" value="Accrual" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiRadioButton } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiRadioButton], template: `<sui-radio-button label="Report basis" name="basis" value="Accrual" (suiChange)="onChange($event)"></sui-radio-button>`, }) export class Example { onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The group is a role="radiogroup" labeled by its own label. The native inputs carry focus and Space, and the arrow keys move the selection with the focus, the way native radios do.

<sui-radio-group id="rb-basic" label="Report basis" name="basis" value="Accrual"></sui-radio-group>
// setup(root): properties and events setup(root) { root.querySelector('#rb-basic').options = bases; }

Descriptions and a row layout

An option can carry a description line, wired to the radio through aria-describedby. orientation="horizontal" lays the options out in a wrapping row instead of a column.

<div style="display:grid;gap:18px;max-width:460px"> <sui-radio-group id="rb-desc" label="Payment terms" name="rb-terms" value="net30"></sui-radio-group> <sui-radio-group id="rb-inline" label="Period" name="rb-period" value="Quarter" orientation="horizontal"></sui-radio-group> </div>
// setup(root): properties and events setup(root) { root.querySelector('#rb-desc').options = terms; root.querySelector('#rb-inline').options = ['Month', 'Quarter', 'Year']; }

Standalone radios

Radios that share a name inside the same form behave as one native group without a wrapper; use this only when the layout has to interleave other content. A lone radio has no help or error line, because it can be checked but never unchecked, so the set carries the message.

<form id="rb-solo-form" style="display:flex;flex-direction:column;gap:12px;align-items:flex-start"> <sui-radio-button name="rb-method" value="ach" label="ACH transfer" description="Free. Lands in 2 to 3 days." checked></sui-radio-button> <sui-radio-button name="rb-method" value="card" label="Credit card" description="2.9% fee. Posts the same day."></sui-radio-button> <sui-radio-button name="rb-method" value="check" label="Paper check" description="Recorded when the deposit clears."></sui-radio-button> </form>

Required standalone radios

required on a radio holds its named group, as on a native radio: until one radio of the group is checked, every radio reports valueMissing and the form does not submit. Inside sui-radio-group the group's own required governs instead, so one question carries one message.

Submit with nothing picked and the form holds.
<form id="rb-req-form" style="display:flex;flex-direction:column;gap:12px;align-items:flex-start"> <sui-radio-button name="rb-plan" value="monthly" label="Monthly" required></sui-radio-button> <sui-radio-button name="rb-plan" value="yearly" label="Yearly"></sui-radio-button> <sui-button type="submit" variant="brand" label="Continue"></sui-button> <output id="rb-req-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit with nothing picked and the form holds.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#rb-req-form'); const out = root.querySelector('#rb-req-out'); form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${v}`).join(' ') || 'Nothing posted.'; }); }

States

The group is the field, so help and error live here and each radio takes only the danger ring through invalid. The label and the message line sit outside the radiogroup, so a screen reader does not read the error again while the user arrows through the options.

<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;align-items:start"> <sui-radio-group id="rb-req" label="Report basis" name="rb-basis-req" required help="The basis sets which invoices appear."></sui-radio-group> <sui-radio-group id="rb-err" label="Report basis" name="rb-basis-err" required error="Report basis is required."></sui-radio-group> <sui-radio-group id="rb-off" label="Report basis" name="rb-basis-off" value="Cash" disabled help="Locked while the period is closed."></sui-radio-group> </div>
// setup(root): properties and events setup(root) { ['#rb-req', '#rb-err', '#rb-off'].forEach((id) => { root.querySelector(id).options = bases; }); }

Read-only and small

readonly keeps the radio named and in the tab order and refuses a click, Space and the arrows; the group still posts. size="sm" draws the 16px circle, the same stop sui-checkbox has.

<div style="display:grid;gap:12px;max-width:420px"> <div style="display:flex;gap:18px"><sui-radio-button name="ro-basis" label="Accrual" value="Accrual" checked readonly></sui-radio-button><sui-radio-button name="ro-basis" label="Cash" value="Cash" readonly></sui-radio-button></div> <div style="display:flex;gap:18px"><sui-radio-button name="sm-basis" label="Accrual" value="Accrual" size="sm" checked></sui-radio-button><sui-radio-button name="sm-basis" label="Cash" value="Cash" size="sm"></sui-radio-button></div> </div>

In a form

The group posts one entry under its own name and the child radios post nothing, so a value can never post twice. An unpicked group posts an empty string.

Submit to see what posts.
<form id="rb-form" style="display:grid;gap:18px;max-width:420px"> <sui-radio-group id="rb-form-basis" label="Report basis" name="basis" required></sui-radio-group> <sui-radio-group id="rb-form-terms" label="Payment terms" name="terms"></sui-radio-group> <sui-button type="submit" variant="brand" label="Save preferences"></sui-button> <output id="rb-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#rb-form'); const out = root.querySelector('#rb-out'); const basis = root.querySelector('#rb-form-basis'); basis.options = bases; basis.value = 'Accrual'; const t = root.querySelector('#rb-form-terms'); t.options = terms; t.value = 'net30'; form.addEventListener('submit', (e) => { e.preventDefault(); const entries = [...new FormData(form)].map(([k, v]) => `${k}=${v}`); out.textContent = entries.length ? entries.join(' ') : 'Nothing posted.'; }); }

Events

The group emits suiChange with the selected value. A child radio emits suiChange with the new checked state, as sui-checkbox does, and the group stops it, so a consumer hears one event with one shape.

Idle.
<div style="display:flex;flex-wrap:wrap;gap:18px;align-items:flex-start"> <sui-radio-group id="rb-ev" label="Report basis" name="rb-basis-ev"></sui-radio-group> <span id="rb-ev-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#rb-ev'); const log = root.querySelector('#rb-ev-log'); el.options = bases; el.addEventListener('suiChange', (e) => { log.textContent = `Picked ${e.detail}.`; }); }

Basicsui-radio-group

options takes plain strings, so the label is also the posted value. orientation="horizontal" lays two short options out in a row instead of a column.

<sui-radio-group label="Report basis" name="basis" value="Accrual" orientation="horizontal"></sui-radio-group>
// setup(root): properties and events setup(root) { root.querySelector('sui-radio-group').options = ['Accrual', 'Cash']; }

Read-only and invalidsui-radio-group

readonly reaches every radio and refuses a change while the group still posts. invalid paints every radio in the danger tone without a message.

<div style="display:grid;gap:18px;max-width:420px"> <sui-radio-group id="rg-ro" label="Report basis" name="basis-ro" value="Cash" orientation="horizontal" readonly></sui-radio-group> <sui-radio-group id="rg-invalid" label="Report basis" orientation="horizontal" invalid></sui-radio-group> </div>
// setup(root): properties and events setup(root) { root.querySelector('#rg-ro').options = ['Accrual', 'Cash']; root.querySelector('#rg-invalid').options = ['Accrual', 'Cash']; }

Keyboard

KeyAction
Tab sui-radio-buttonMoves to the checked radio, the one tab stop for the group. While none is checked, every radio is a tab stop.
ArrowDown ArrowRight sui-radio-buttonChecks and focuses the next enabled radio, wrapping to the first. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one.
ArrowUp ArrowLeft sui-radio-buttonChecks and focuses the previous enabled radio, wrapping to the last.
Space sui-radio-buttonChecks the focused radio, through the native input. A checked radio never unchecks.

API

<sui-radio-button>

Properties

PropertyAttributeTypeDefaultDescription
checkedcheckedbooleanfalseChecked state. Mutable: the element checks itself on user input, then emits suiChange. Reflected, so sui-radio-button[checked] matches the radio as it is now.
descriptiondescriptionstringstring | undefinedSecondary line under the label, describing this option; wired through aria-describedby. Not a field help line.
disableddisabledbooleanfalseDisables this radio. The arrow keys skip it and it posts nothing.
invalidinvalidbooleanfalseDanger ring without a message. A radio has no help or error of its own: one message per option would repeat, and would describe the choice from inside a single option. The message belongs to sui-radio-group, which sets this on every child while it has an error.
labellabelstringstring | undefinedVisible label.
namenamestringstring | undefinedGroup name. Radios sharing a name inside the same form, or the same sui-radio-group, are one native group: checking one unchecks the others. Reflected: the form entry is built from the content attribute.
readOnlyreadonlybooleanfalseDisplay only: the radio keeps its name and its tab stop but a click, Space or an arrow key changes nothing, and it still posts. sui-radio-group sets this on every child.
requiredrequiredbooleanfalseA radio of this group must be checked for the surrounding form to be valid, as on a native radio: while any radio of a named group is required and none of the group is checked, every radio of the group reports valueMissing, and checking any one of them clears it. The input carries aria-required. Inside sui-radio-group the group's own required governs and this one is not read: the group is the field, holds the form and draws the message, and a child holding it too would put two messages on one question.
sizesize"md" | "sm"'md'Circle size: md 18px (default), sm 16px, as on sui-checkbox.
valuevaluestringstring | undefinedValue posted with the form while checked. Inside a sui-radio-group the group posts instead.

Events

EventDetailReactVueDescription
suiChangebooleanonSuiChange@sui-changeFires when this radio becomes checked by user input; detail is the new checked state, as on sui-checkbox, sui-toggle-switch and sui-toggle-button. A radio only ever reports itself becoming checked, so the detail is always true.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the (hidden) native input, as el.focus() does; the drawn circle shows the focus ring.

<sui-radio-group>

Properties

PropertyAttributeTypeDefaultDescription
disableddisabledbooleanfalseDisables every radio. A disabled group posts nothing and never holds the form.
errorerrorstringstring | undefinedError message; paints every radio in the danger tone and sets aria-invalid.
helphelpstringstring | undefinedHelp text under the options. Replaced by error when set.
invalidinvalidbooleanfalseDanger ring on every radio without a message, for a form that draws its summary elsewhere.
labellabelstringstring | undefinedGroup label (12.5px / 620).
namenamestringstring | undefinedForm control name, also shared by the child radios. Reflected: the form entry is built from the content attribute.
optionsproperty onlySuiRadioGroupOptionInput[][]Options: strings or numbers, or { label, value, description, disabled } objects. A disabled option cannot be picked and the arrow keys skip it. Property, not attribute.
orientationorientation"horizontal" | "vertical"'vertical'vertical (the default) stacks the options in a column with a 9px gap; horizontal lays them out in a wrapping row with an 18px gap. The word every element with an axis uses. Reflected.
readOnlyreadonlybooleanfalseDisplay only: every radio keeps its name and its tab stop but changes nothing, and the group still posts. Neither a read-only nor a disabled group holds the form on valueMissing.
requiredrequiredbooleanfalseA selection is required for the surrounding form to be valid.
valuevalueSuiKeynumber | string | undefinedThe picked option's value, as the option gives it (a number stays a number). Written as an attribute it is text, and it still picks the option whose value reads the same. Mutable; the group updates it before emitting suiChange.

Events

EventDetailReactVueDescription
suiChangeSuiKeynumber | stringonSuiChange@sui-changeFires when the selection changes; detail is the new value, the picked option's value.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the checked radio, or to the first one that can take it, as el.focus() does. sui-form calls this to reach the first invalid control.

Types

SuiKey typeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.
SuiRadioGroupOption interfaceOne option. value is what value holds, as given: a number stays a number through value, suiChange and a framework binding. The form posts its text.
labelstring
valueSuiKey
description?string
disabled?boolean
SuiRadioGroupOptionInput typeAn option: a string or a number stands for itself as label and value.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also