J3W1 / DESIGN SYSTEM
Radio group
A fieldset of native radios for one exclusive choice; each radio is a 16px square that fills with the primary action colour and an inner square when checked.
stable · LIGHT DOM · 1.1.0
Light DOM retains static content, native semantics, label relationships and host composition without duplicating controls.
Live variants
default
<j3w1-radio-group id="example-radio-group-default"><fieldset class="radio-group" aria-describedby="example-radio-group-default-rg-layout-message">
<legend class="radio-group-legend">Default layout <span class="radio-group-required" aria-hidden="true">*</span><span class="radio-group-required sr-only"> required</span></legend>
<div class="radio-group-options">
<label class="radio-group-option">
<span class="radio-group-control">
<input class="radio-group-input" type="radio" name="example-radio-group-default-layout" value="split">
<span class="radio-group-dot" aria-hidden="true"></span>
</span>
<span class="radio-group-text">Split horizontally</span>
</label>
<label class="radio-group-option">
<span class="radio-group-control">
<input class="radio-group-input" type="radio" name="example-radio-group-default-layout" value="stacked">
<span class="radio-group-dot" aria-hidden="true"></span>
</span>
<span class="radio-group-text">Stacked</span>
</label>
<label class="radio-group-option">
<span class="radio-group-control">
<input class="radio-group-input" type="radio" name="example-radio-group-default-layout" value="tabbed">
<span class="radio-group-dot" aria-hidden="true"></span>
</span>
<span class="radio-group-text">Tabbed</span>
</label>
</div>
<p class="radio-group-message" id="example-radio-group-default-rg-layout-message">✕ Choose a layout.</p>
</fieldset></j3w1-radio-group>horizontal
<j3w1-radio-group id="example-radio-group-horizontal"><fieldset class="radio-group radio-group-horizontal" aria-describedby="example-radio-group-horizontal-rg-density-message">
<legend class="radio-group-legend">Density <span class="radio-group-required" aria-hidden="true">*</span><span class="radio-group-required sr-only"> required</span></legend>
<div class="radio-group-options">
<label class="radio-group-option">
<span class="radio-group-control">
<input class="radio-group-input" type="radio" name="example-radio-group-horizontal-density" value="compact">
<span class="radio-group-dot" aria-hidden="true"></span>
</span>
<span class="radio-group-text">Compact</span>
</label>
<label class="radio-group-option">
<span class="radio-group-control">
<input class="radio-group-input" type="radio" name="example-radio-group-horizontal-density" value="comfortable">
<span class="radio-group-dot" aria-hidden="true"></span>
</span>
<span class="radio-group-text">Comfortable</span>
</label>
</div>
<p class="radio-group-message" id="example-radio-group-horizontal-rg-density-message">✕ Choose a density.</p>
</fieldset></j3w1-radio-group>Install and register
Use the exact version. If npm publication is pending, install the release tarball from the installation guide.
npm install --save-exact @j3w1/ui@1.1.0import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/radio-group.css';
import '@j3w1/ui/register/radio-group';Insert the maintained markup above after loading these imports. Its native children provide accessible names and form behavior; the empty custom tag alone does not create a control.
Copy the complete implementation
npx --package=@j3w1/ui@1.1.0 j3w1-ui copy radio-group --out ./vendor/j3w1/radio-groupThe CLI verifies every file before writing and refuses overwrites. Serve the output over HTTP and open index.html. Keep its modules, CSS, tokens and licence notices together.
Public API
attributes
| Name | Type / signature | Meaning |
|---|---|---|
disabled | CustomEvent | Disable the native controls owned by this instance. |
loading | CustomEvent | Mark the primary control busy and prevent duplicate activation. |
properties
| Name | Type / signature | Meaning |
|---|---|---|
disabled | boolean | Delegates disabled state to native children. |
name | string | Native control name; application form keys remain explicit. |
methods
| Name | Type / signature | Meaning |
|---|---|---|
refresh | (): void | Reconnect behavior after replacing native child structure. |
focus | (options?: FocusOptions): void | Focus the first usable native control. |
events
| Name | Type / signature | Meaning |
|---|
Dependencies and verification
Registered components: none. Style dependencies: none.
Package presence establishes an available implementation. Actual passes depend on the tested artifact and environment. Read specification execution evidence; read packed-consumer evidence; inspect the complete state matrix.
Full implementation manifest, API and token dependency closure
Canonical specification
Purpose
One choice out of a few visible options. Below two options a checkbox or switch is used; above roughly six, a select. The radios are square, as every control in this theme is, and are told apart from checkboxes by the inner square instead of the ✓ glyph and by arrow-key movement instead of separate tab stops.
Anatomy
A <fieldset> whose <legend> asks the question; a list of <label> rows
each holding the native input, drawn as a 16px square with appearance: none, and its text; the validation message at the end. The inner square is
a real element stacked over the input in the same cell.
States
| State | Visual | Non-colour channel |
|---|---|---|
| default | 16px square, 1px color.border.control on color.surface.input; text color.text.default | — |
| hover | box background → color.interaction.hover.bg; a checked box → color.action.primary.hover-bg | cursor: pointer; hover-capable pointers only |
| focus-visible | ring 1px dashed color.interaction.focus.ring at −2px around the box | the ring |
| checked | box fills color.action.primary.bg; an 8px inner square in color.action.primary.text; border stays color.border.control | the inner square; checked |
| checked+focus-visible | the fill and a ring in color.interaction.focus.ring-container | inner square and ring |
| disabled | boxes color.interaction.disabled.bg with 1px color.border.disabled; inner square and text color.text.disabled | disabled; cursor: not-allowed |
| invalid | every box border 2px color.status.danger.border; message with the ✕ glyph | border width 1 → 2px; glyph; aria-invalid |
| required | * after the legend in color.status.danger.text plus visually hidden “required” | required; the mark |
Precedence: disabled > invalid > checked > hover; focus-visible is always drawn.
Keyboard
Native. The group is one tab stop; the arrow keys move both focus and the check, so moving through a group changes the value. That is why a radio group never triggers navigation or a request on change; the value is read on submit or through an explicit action.
Accessibility
The legend is read before each option and is the group’s name. Options
share a name. The message is linked with aria-describedby on the
fieldset and aria-invalid is set on the inputs after submit. A group with
no default checked radio must have a required mark or a “none” option, so
the empty state is intentional. Contrast: text 8.43:1, inner square 9.28:1
on the fill, box edge 4.45:1 on the input surface and 4.33:1 against the
panel when checked, ring 4.69:1, container ring 5.57:1.
Portability
Square, border, fill and one inner square; no pseudo-element is needed. Hosts that insist on a circular radio indicator record the deviation rather than replacing the control with a custom widget.
Non-examples
Circular radios. A checked state shown by border colour alone with no inner
square. Radios that submit or navigate on change. A group with no legend or
with the legend rendered as a label on one option. Separate tab stops per
radio. One radio on its own. Disabling by opacity. A div with
role="radiogroup" built from spans.