Radio
An enhanced native <input type="radio"> supporting group keyboard navigation (radiogroup pattern), data-driven options, and a card variant.
Basic Usage
Basic usage
Radios sharing a name are mutually exclusive (the component syncs across Shadow DOM boundaries); without a name each stays independent — use a shared name or oas-radio-group for exclusivity.
Sizes
Three sizes (size)
size controls both the dot size and the font size; set on the group it cascades to all items, while an explicit item size wins.
Radio Group (Keyboard Accessible)
Radio group
The group is controlled via value; each item's value acts as the option identifier, and the group manages exclusivity.
The group follows the WAI-ARIA radiogroup keyboard pattern: Tab enters the group once (stopping at the checked item, or the first non-disabled / non-readonly item when none is checked), ↑ ↓ ← → move and select immediately (wrapping around, skipping disabled items), Home / End jump to the first/last enabled item. Native inputs each live inside their own Shadow DOM, so the browser's same-name arrow grouping breaks across shadow boundaries — the component fills the gap with a host-level roving tabindex and arrow key handling.
Single Disabled
Single disabled
A single item's disabled only disables that item: not clickable, not focusable (native disabled semantics); the rest of the group is unaffected. Disable the whole group via oas-radio-group's disabled (below).
Disabled
Disabled
Readonly
Readonly
readonly and disabled carry distinct form semantics: focusable, reachable via Tab, value still submits, but clicks (and Space) plus arrow keys never switch. Can also be set per item.
Validation Status
Validation status (status, per-item supported)
A group-level status cascades to all items; an explicit item status wins over the group value. status="error" automatically sets aria-invalid="true"; in the card variant the status color lands on the border.
Description & Label Position
Description (description) and label position (label-position)
The description attribute renders as secondary text below the label (per-option subtext); a <span slot="description"> can distribute rich content instead (takes priority over the attribute). label-position="start" moves the text to the start side of the dot (mirrors automatically in RTL).
Custom Indicator
Custom indicator (checked-icon slot)
Slot content takes over the checked-state icon (template[slot] or a direct element both work); the unchecked state falls back to a default hollow circle, overridable via ::part(box) (see the next example: both states customized).
Both states customized (checked-icon slot + ::part(box))
The custom indicator is a per-item switch: only items carrying the
checked-iconslot enter custom mode (native dot hidden,.boxbecomes the unchecked carrier) — that is why both items above carry the slot. For a uniform look across a group, every item needs it.
Horizontal Group
Horizontal direction (direction)
Defaults to direction="vertical" (stacked); horizontal lays items out in a row and wraps automatically.
Data-Driven
options data channel
The options JSON attribute and declarative child elements form dual channels: when options is explicit, data-driven rendering wins (rendered internally; declarative children step aside). Fields: label / value / disabled / description. Data-driven items participate in keyboard navigation as usual.
Card Variant
Card variant (variant=card)
variant="card" switches to a card skin: the whole block is clickable, the selected state tints the border, hover gives feedback; put titles in the default slot and use description for subtext. Card items can join a group (driven by the group value, keyboard navigation intact) or stand alone.
Events
Events (oas-change / oas-focus / oas-blur)
An item dispatches oas-change (detail: { checked, value }) plus oas-focus / oas-blur; the group dispatches oas-change (detail: { value }, fired by both clicks and arrow-key selection) and group-level oas-focus / oas-blur (moves between items do not misfire).
required
required (native validation chain)
required drives the native validation chain (form-associated): when unfilled, checkValidity() returns false (valueMissing) and native form submission is blocked; it recovers to :valid once filled.
required also works at the group level: oas-radio-group required uses the native form-associated validation chain — when no option is selected, valueMissing blocks native form submission (FormData submits the selected value under the group's name; nothing is submitted when empty).
Group-level required (radio-group)
Click submit without selecting: the browser shows the valueMissing validation bubble, the form is not submitted, and the page asks you to pick a plan first; select an option and submit again to see the submitted value.
The current value is available through the public value property for read/write (mirroring the native radio.value): submission-value semantics — falls back to 'on' when the value attribute is absent, independent of the checked state (whether it is submitted is decided by checked); el.value = 'x' writes the controlled value attribute and refreshes the FormData submission value. No events are dispatched.
API
oas-radio
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
checked | Whether checked | boolean | — |
description | Secondary text rendered below the label; a slot="description" distribution takes priority | string | — |
disabled | Disabled | boolean | — |
label-position | Label position: end (default, dot left / text right) / start (text left; mirrors automatically in RTL) | — | — |
name | Native grouping name (same-name exclusivity across Shadow DOM; a group assigns a unique name automatically) | string | — |
readonly | Readonly: focusable and Tab-reachable, value still submits, but clicks (and Space) plus arrow keys never switch | boolean | — |
required | Required marker (per-element semantics: this item unchecked means valueMissing; group-level any-checked semantics are a known boundary) | boolean | — |
size | Size: small (14px) / medium (default 16px) / large (18px), dot and font scale together; a group-level value cascades to items while an explicit item value wins | string | medium |
status | Validation status: error / warning / success (tints the dot; error also sets host aria-invalid) | string | — |
value | Option identifier | string | null | — |
variant | Variant: default / card (whole block clickable, selected border tint, hover feedback) | string | — |
Property (JS property; see each row for attribute reflection)
| Property | Description | Type | Default |
|---|---|---|---|
value | The submitted value (attribute default on; separate from checked, matching native radio.value) | string | null | — |
Events
| Event | Description |
|---|---|
oas-blur | The radio lost focus |
oas-change | Selection change, detail: { checked, value } |
oas-focus | The radio gained focus |
Slots
| Name | Description |
|---|---|
| default | Label content (rich text/links allowed) |
checked-icon | Custom checked indicator (template[slot] or a direct element; unchecked falls back to the default hollow circle) |
description | Description distribution channel (takes priority over the description attribute) |
oas-radio-group
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
direction | Layout direction: vertical (default) / horizontal (row layout with wrapping) | string | vertical |
disabled | Disable the whole group (cascades to items without overriding their own explicit disabled) | boolean | — |
options | Data channel: JSON [{ label, value, disabled?, description? }]; when explicit, data-driven rendering wins over declarative children | RadioOption[] | string | — |
readonly | Readonly (cascades to items): focusable, arrow keys do not switch | boolean | — |
required | Group-level required validation via the form-associated chain (valueMissing); reset restores the baseline | boolean | — |
size | Size (cascades to items): small / medium (default) / large | string | — |
status | Validation status (cascades to items): error / warning / success; an explicit item status wins | string | — |
value | Group value (the selected item value) | string | — |
Events
| Event | Description |
|---|---|
oas-blur | Fired when focus leaves the group |
oas-change | Group value change, detail: { value } (fired by both clicks and arrow-key selection) |
oas-focus | Any item inside the group gained focus (moves between items do not misfire) |
Slots
| Name | Description |
|---|---|
| default | Declarative item channel (<oas-radio> children; steps aside when options is explicit) |
label | Group title (rendered into the legend) |