Noctis

A single-select control. The user picks exactly one option with the pointer or the keyboard; the selected radio carries an accent border and a dot that scales in, on a true-circle box that stays round at any radius setting. A real form control — it forwards name / form / required / readOnly and can flag an invalid choice.

Basic

Compose Radio.Group (which owns the value) with one Radio.Field per option — each pairing a Radio box with its Radio.Label, the same first-class label idiom as Checkbox. The field wires the label to the box (clicking the label selects it, the label is the accessible name) and holds the box↔label gap; one size on the group sizes every row. Name the group with aria-label (or aria-labelledby), and drive it with value / onValueChange or an uncontrolled defaultValue.

Orientation

A group stacks vertically by default; orientation="horizontal" lays it out in a wrapping row. Arrow keys navigate both axes, and the row mirrors under RTL by construction.

Sizes

size on the group sets the box edge length of every radio — sm for dense rows, md (the default), or lg for cards and touch. The accent dot re-points with it, so it stays proportional.

Descriptions

Add a Radio.Description per option for helper text, wired to its radio with aria-describedby. Keep the description a sibling of the Radio.Field so it isn't folded into the accessible name.

1 project, community support.
Unlimited projects and history.
Shared workspaces and roles.

Validation

Mark the group required and flag it invalid to surface a validation error: the danger border paints on every radio and the error message is wired through aria-describedby. Validation fires at submit, not on blur — colour is never the only signal.

Cards

Radio.Card is a selectable card: a sharp bordered target wrapping a radio, so the whole card toggles. Selecting it tints the edge to the accent; hover is a neutral surface shift and the focus ring moves to the card. The control sits at the leading edge by default, or the trailing edge via controlPosition.

Disabled

Set disabled on a single Radio to lock one option inside an otherwise enabled group — arrow navigation skips it. Set disabled on the whole Radio.Group to dim and lock every option at once.

Standalone

A radio can be named by aria-label instead of visible text — an icon-style picker where the option is self-evident. It is still a group, so arrows and single-select apply.

Keyboard

KeyAction
TabMove focus into the group, landing on the selected radio (or the first); Tab again leaves the group.
/ / / Move to the next / previous radio and select it (selection follows focus), wrapping at the ends. Arrows mirror under RTL.
SpaceSelect the focused radio.

A disabled option inside an enabled group is skipped by the arrows. A radio group is a single composite tab stop — Tab advances to the next field, not the next radio.

Accessibility

  • Roles. Radio.Group is a radiogroup (named by aria-label / aria-labelledby / a <legend>); each Radio is a radio, named by its Radio.Label (or an aria-label for a label-less radio). aria-orientation reflects the orientation prop.
  • Validation. invalid sets aria-invalid on the group; pair it with an error message wired by aria-describedby. The danger border is a reinforcement, never the only signal.
  • Read-only. A read-only radio stays focusable and announced — reflected visually but never removed from the accessibility tree.
  • Descriptions. Radio.Description is associated to its radio with aria-describedby (the consumer sets the matching id).
  • Motion. The dot scales / fades in on select; under prefers-reduced-motion: reduce it appears instantly, meaning preserved.

Anatomy

Compose the control from its parts. Radio.Group is a Base UI RadioGroup, so selection, roving focus, arrow-key navigation, and the form surface (name / form / required / readOnly) come for free.

  • Radio.Group — the group; owns the selected value (value / onValueChange, or uncontrolled defaultValue), the shared size, and the orientation. Forwards name / form / required / readOnly to Base UI; flag the value in error with invalid; disable everything with disabled.
  • Radio — one radio box. Pass its value (and optional readOnly / required); it renders the accent dot (Radio.Indicator) for itself when selected.
  • Radio.Field — the labelled row pairing a box with its label (Base UI's Field.Root), mirroring Checkbox.Field. Inherits the group's size unless it sets its own, and cascades it to the box.
  • Radio.Label — the box's visible accessible name (Base UI's Field.Label); clicking it selects the box, and it mirrors the field's disabled / invalid state. For a label-less radio (an icon picker), give the Radio an aria-label instead.
  • Radio.Indicator — the accent dot; kept mounted so it scales in on select. Radio renders it automatically.
  • Radio.Description — muted helper text for a radio, wired by aria-describedby.
  • Radio.Card — a selectable card target wrapping a radio, its label, and an optional description.

Every rendered part carries a data-slot (noctis-radio-group, noctis-radio, noctis-radio-indicator, noctis-radio-field, noctis-radio-label, noctis-radio-description, noctis-radio-card) for host-side styling — pair it with the Base UI radio state attributes (data-checked, data-unchecked, data-disabled, data-readonly, data-invalid) and the data-size / data-orientation axes.

On surfaces

The same control re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well. It stays legible on every layer.

root
elevated
menu
sunken

Theme scope

ThemeScope re-tunes a whole region's shape — corner radius, density, and type scale — through the cascade, with no per-component prop and without touching the app-wide theme. Tweak the knobs below and watch every radio inside the scope retune together. See Customization for nesting, reset, and the portal caveat.

Design tokens

Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every radio in that region retunes — e.g. .dense { --noctis-radio-group-gap: 0.375rem; } tightens the stack beneath it. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.

Token

API reference

Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; the Group forwards the Base UI RadioGroup props it owns selection through. Expand a row for the full type and description.

Radio.Group

Prop

Radio.Root

Prop

Radio.Indicator

Prop

Radio.Description

Prop

Radio.Card

Prop

AttributeDescription
data-slotThe styling/testing hook: the part's stable slot name.
data-sizeSize axis on the radio box — `sm` | `md` | `lg`; re-points the box and dot edges.
data-orientationLayout axis on the group — `vertical` | `horizontal`; switches the row/column flow.
data-checkedPresent on the selected radio box (and its indicator).
data-uncheckedPresent on an unselected radio box.
data-disabledPresent on a disabled radio or group.
data-readonlyPresent on a read-only radio — valid and announced, but not editable.
data-invalidPresent on an invalid radio (and on a group flagged `invalid`); also honours `aria-invalid`.
data-controlOn a `Radio.Card`, where the control sits — `leading` | `trailing`.
data-control-hiddenOn a `Radio.Card` with `hideControl`, marks the dot hidden so the accent edge is the only signal.