Skip to content

Radio

Terminal-styled radio button and radio group for single-selection.

Preview

Installation

$pnpm exec dgadd add ui/radio
[Installs to]src/components/ui/radio[Item]ui/radio

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

Usage

tsx
"use client";import { useState } from "react";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio";export default function RadioGroupDefault() {  const [value, setValue] = useState("react");  return (    <RadioGroup value={value} onChange={setValue} label="Framework">      <RadioGroupItem value="react" label="React" />      <RadioGroupItem value="vue" label="Vue" />      <RadioGroupItem value="svelte" label="Svelte" />      <RadioGroupItem value="angular" label="Angular" />    </RadioGroup>  );}

Examples

Variants

Preview

Highlight, Invalid, and Disabled

Preview

API Reference

Radio

NameTypeDefaultDescription
checkedbooleanControlled checked state.
defaultCheckedbooleanfalseInitial checked state for uncontrolled usage.
onChange(checked: boolean) => voidCalled when the boolean checked state changes.
valuestring"on"Hidden native input value used for form submission.
namestringHidden native input name used for same-name radio behavior and form submission.
requiredbooleanfalseMarks the hidden native radio input as required.
labelReactNodeVisible label associated with the custom radio.
descriptionReactNodeVisible description wired with aria-describedby.
disabledbooleanfalseDisables the custom control and hidden input.
size"sm" | "md" | "lg""md"Selectable control size token.
variant"x" | "bullet""bullet"Indicator style.

RadioGroup

NameTypeDefaultDescription
valuestringControlled selected value.
defaultValuestringInitial selected value for uncontrolled usage.
onChange(value: string) => voidCalled when the selected value changes.
highlightedstring | nullControlled highlighted item value for keyboard navigation.
onHighlightChange(value: string | null) => voidCalled when keyboard navigation highlights a new item or clears highlight.
onNavigate(value: string, direction: "previous" | "next" | "first" | "last") => voidCalled when arrow, Home, or End navigation moves to an item.
onEnter(value: string, event: KeyboardEvent) => voidCalled when Enter commits the focused item.
orientation"vertical" | "horizontal""vertical"Layout orientation. All four arrow keys still navigate per APG radio behavior.
activationMode"automatic" | "manual""automatic"Automatic selects on arrow navigation; manual moves focus/highlight until Space or Enter commits.
wrapbooleantrueWhether arrow-key navigation wraps at the first and last item.
keyboardNavigationbooleantrueEnable built-in arrow-key navigation.
onNavigationBoundaryReached(direction: "previous" | "next", event: KeyboardEvent, key: string) => voidCalled when non-wrapping navigation reaches the first or last item.
autoFocusbooleanfalseFocuses the highlighted, selected, or first enabled item when the group becomes active.
namestringShared hidden native input name for grouped form submission.
requiredbooleanfalseRequires one enabled item to be selected. All-disabled groups are exempt like native radios.
labelstringVisible group label rendered before the items. Also names the radiogroup unless aria-label overrides it.
aria-labelstringExplicit accessible name for the radiogroup. Overrides the visible label when supplied.
aria-labelledbystringID of an external element that labels this component. Composed with the visible label unless aria-label is supplied.

RadioGroupItem

NameTypeDefaultDescription
valuerequiredstringItem value. Must be unique within the group.
labelrequiredReactNodeVisible item label.
descriptionReactNodeVisible item description wired with aria-describedby.
disabledbooleanfalseDisables the item.

Data attributes

AttributeApplies toValuesDescription
data-stateRadio / RadioGroupItem"checked" | "unchecked"Boolean visual state for styling the custom control.
data-disabledRadio / RadioGroupItempresent when disabledMarks disabled radios.
data-highlightedRadio / RadioGroupItempresent when highlightedMarks the current keyboard-highlighted group item.
data-valueRadio / RadioGroupItemitem valueStable value used by group navigation and form submission wiring.
data-diffgazer-selectable-ownerRadioGroup"radio"Scopes nested selectable-item discovery for keyboard navigation.

Accessibility

Keyboard Navigation

All four arrow keys, plus the vim aliases j/k, move focus and select items in automatic activation mode (per WAI-ARIA APG). Manual activation mode moves focus and emits onNavigate without changing value. Space selects the focused item. Enter commit via onEnter is a Diffgazer extension for preview/commit flows. Home/End jump to first/last item. Composite UIs can opt into initial focus with autoFocus, suspend RadioGroup-managed key handling with keyboardNavigation, and listen for onNavigationBoundaryReached(direction, event, key).

KeyAction
ArrowUp / ArrowLeft / kMoves focus to the previous enabled radio; automatic mode also selects it.
ArrowDown / ArrowRight / jMoves focus to the next enabled radio; automatic mode also selects it.
Home / EndMoves focus to the first or last enabled radio and selects it in automatic mode.
SpaceSelects the focused radio.
EnterCalls onEnter for preview/commit flows, or selects when wired.

Arrow navigation in both orientations

Preview

Notes

Requires @diffgazer/keys (package mode)

RadioGroup's roving keyboard navigation imports from the required @diffgazer/keys peer. Package consumers must install @diffgazer/keys with @diffgazer/ui; packages are not yet published to npm, so until the first release install both from locally packed tarballs. Importing @diffgazer/ui/components/radio without keys fails at module load with an error naming the missing @diffgazer/keys package. Copy/dgadd consumers do not need the package — copy mode rewrites the keyboard hooks to local source.

Composition Contract

Prefer importing RadioGroup and RadioGroupItem for shadcn-like code. RadioGroup.Item is the same item component exposed for Radix-like namespacing. Use either form inside RadioGroup, directly or through your own wrapper components. Every item value must be unique within a group because selection, highlighting, and form output are value-based.

Standalone vs RadioGroup

Standalone same-name Radio components stay mutually exclusive via a document-level event, but they are form-submission-only: there is no arrow-key navigation and each radio is its own tab stop. For a keyboard-complete group (roving focus, arrow navigation, a single tab stop), use RadioGroup.

Keyboard Navigation

RadioGroup delegates roving focus to @diffgazer/keys navigation. All four arrow keys move focus and select items by default regardless of orientation (per WAI-ARIA APG radio group pattern). Home/End jump to first/last item. Space selects the focused item. Enter commit is a Diffgazer extension for preview/commit flows, not the APG baseline. Use activationMode="manual" with onNavigate/onChange when arrows should move focus and highlight without changing value, and onEnter when Enter should commit the focused item. Use autoFocus to focus the highlighted, selected, or first enabled item when the group becomes active. Use keyboardNavigation to suspend RadioGroup-managed key handling; when suspended, enabled items remain tabbable. Use onNavigationBoundaryReached(direction, event, key) to hand focus to adjacent controls and filter vertical-only handoffs by key when needed.

Glyph Hierarchy

The brackets are chrome and render muted; the inner mark keeps the control's tone at bold weight, so a selected row is the only place full-contrast ink appears in the glyph column. The visible text is unchanged and the whole cell stays aria-hidden.

Indicator Variants

variant='bullet' (default) marks the selected state with [●]; variant='x' marks it with [x]. Both glyphs are three characters, the same width Checkbox reserves, so a form mixing radios and checkboxes keeps one label left edge at every size.

Orientation

Set orientation='horizontal' for inline layouts. Layout direction changes but all four arrow keys always navigate (per APG spec).

Required validation

A required RadioGroup participates in native validation only while it has an enabled item. If every item is disabled, the group follows native radio behavior: it is valid, omits aria-required and derived invalid state, contributes no value, and has no validation focus target.

Source

Install via CLI: pnpm exec dgadd add ui/radio. Keyboard hooks are included as standalone copies. For the full experience, use --integration keys.

Highlighted source loads after this disclosure opens. Browse the source repository.