<script setup>
import { ref } from "vue";
import { Radio } from "@dicehub/kappa/components/radio";
const density = ref("comfortable");
</script>
<template>
<Radio.Root v-model="density" name="interface-density">
<Radio.Label>Interface density</Radio.Label>
<Radio.Item value="compact">
<Radio.ItemControl />
<Radio.ItemText>Compact</Radio.ItemText>
</Radio.Item>
<Radio.Item value="comfortable">
<Radio.ItemControl />
<Radio.ItemText>Comfortable</Radio.ItemText>
</Radio.Item>
<Radio.Item value="spacious">
<Radio.ItemControl />
<Radio.ItemText>Spacious</Radio.ItemText>
</Radio.Item>
</Radio.Root>
</template>Installation
Barrel
import { Radio } from "@dicehub/kappa";Granular
import { Radio } from "@dicehub/kappa/components/radio";Usage
Put every related option in one Root. Give the group aLabel, and give every Item a unique string value,ItemControl, and ItemText. Each item renders its required native radio input automatically.
<script setup>
import { Radio } from "@dicehub/kappa/components/radio";
</script>
<template>
<Radio.Root default-value="email" name="notification-channel">
<Radio.Label>Notification channel</Radio.Label>
<Radio.Item value="email">
<Radio.ItemControl />
<Radio.ItemText>Email</Radio.ItemText>
</Radio.Item>
<Radio.Item value="sms">
<Radio.ItemControl />
<Radio.ItemText>SMS</Radio.ItemText>
</Radio.Item>
<Radio.Item value="push">
<Radio.ItemControl />
<Radio.ItemText>Push notification</Radio.ItemText>
</Radio.Item>
</Radio.Root>
</template>Composition
Radio.Root
├── Radio.Label
├── Radio.Indicator (optional)
├── Radio.Item
│ ├── HiddenInput (automatic)
│ ├── Radio.ItemControl
│ └── Radio.ItemText
└── Radio.Item
├── HiddenInput (automatic)
├── Radio.ItemControl
└── Radio.ItemTextArk UI owns selection, roving focus, arrow-key movement, form reset, state attributes, and indicator measurement. Kappa owns the compound exports, automatic item inputs, dense geometry, state styling, and semantic tokens.
Rootowns one selected string ornull.Labelnames the group and moves focus into it when clicked.Itemstays a semantic label and always renders its native input.ItemControlrenders a default dot; its slot can replace that mark.ItemTextsupplies the accessible option name and can hold rich text.Indicatoris optional. Ark UI measures the selected item and moves the surface.
Kappa intentionally does not expose asChild on Radio.Item. This keeps its label and hidden input structure valid. Other parts keep Ark UI'sasChild contract. See theArk UI Radio Groupguide for the primitive behavior contract.
<Radio.Root>
<Radio.Label />
<Radio.Indicator />
<Radio.Item value="option">
<!-- Native radio input renders automatically. -->
<Radio.ItemControl />
<Radio.ItemText />
</Radio.Item>
<Radio.Context />
</Radio.Root>Examples
Basic
Use one root for one mutually exclusive set of options.
<script setup>
import { Radio } from "@dicehub/kappa/components/radio";
</script>
<template>
<Radio.Root default-value="system" name="theme">
<Radio.Label>Theme</Radio.Label>
<Radio.Item value="light">
<Radio.ItemControl />
<Radio.ItemText>Light</Radio.ItemText>
</Radio.Item>
<Radio.Item value="dark">
<Radio.ItemControl />
<Radio.ItemText>Dark</Radio.ItemText>
</Radio.Item>
<Radio.Item value="system">
<Radio.ItemControl />
<Radio.ItemText>System</Radio.ItemText>
</Radio.Item>
</Radio.Root>
</template>Descriptions
Compose titles and supporting text inside ItemText when each choice needs more context.
<script setup>
import { Radio } from "@dicehub/kappa/components/radio";
const options = [
{ value: "hourly", label: "Hourly", description: "Keep the latest 24 restore points." },
{ value: "daily", label: "Daily", description: "Keep one restore point for 30 days." },
{ value: "weekly", label: "Weekly", description: "Keep one restore point for 12 weeks." },
];
</script>
<template>
<Radio.Root default-value="daily" name="backup-frequency">
<Radio.Label>Backup frequency</Radio.Label>
<Radio.Item v-for="option in options" :key="option.value" :value="option.value">
<Radio.ItemControl />
<Radio.ItemText class="option-copy">
<span class="option-title">{{ option.label }}</span>
<span class="option-description">{{ option.description }}</span>
</Radio.ItemText>
</Radio.Item>
</Radio.Root>
</template>
<style scoped>
.option-copy { display: grid; gap: 0.125rem; }
.option-title { font-weight: 500; }
.option-description { color: var(--kappa-subtle); font-size: 0.75rem; }
</style>Horizontal
Set orientation to horizontal for a short set that can wrap safely.
<script setup>
import { Radio } from "@dicehub/kappa/components/radio";
</script>
<template>
<Radio.Root default-value="newest" name="sort-order" orientation="horizontal">
<Radio.Label>Sort order</Radio.Label>
<Radio.Item value="newest"><Radio.ItemControl /><Radio.ItemText>Newest</Radio.ItemText></Radio.Item>
<Radio.Item value="oldest"><Radio.ItemControl /><Radio.ItemText>Oldest</Radio.ItemText></Radio.Item>
<Radio.Item value="name"><Radio.ItemControl /><Radio.ItemText>Name</Radio.ItemText></Radio.Item>
</Radio.Root>
</template>Choice Cards
Style each Item as a bordered row when the choice needs a larger click target and description.
<script setup>
import { Radio } from "@dicehub/kappa/components/radio";
const plans = [
{ value: "starter", label: "Starter", description: "5 GB for personal projects." },
{ value: "team", label: "Team", description: "100 GB with shared access." },
{ value: "business", label: "Business", description: "1 TB with audit history." },
];
</script>
<template>
<Radio.Root default-value="team" name="storage-plan" class="choice-cards">
<Radio.Label>Storage plan</Radio.Label>
<Radio.Item v-for="plan in plans" :key="plan.value" :value="plan.value" class="choice-card">
<Radio.ItemControl />
<Radio.ItemText class="choice-card__copy">
<strong>{{ plan.label }}</strong>
<span>{{ plan.description }}</span>
</Radio.ItemText>
</Radio.Item>
</Radio.Root>
</template>
<style scoped>
:deep(.choice-cards.kappa-radio) { width: min(100%, 26rem); }
:deep(.choice-card) {
width: 100%;
border: 1px solid var(--kappa-line);
border-radius: 0.5rem;
padding: 0.75rem;
}
:deep(.choice-card[data-state="checked"]) {
border-color: var(--kappa-accent);
background: color-mix(in oklab, var(--kappa-accent) 5%, transparent);
}
:deep(.choice-card__copy) { display: grid; gap: 0.125rem; }
:deep(.choice-card__copy span) { color: var(--kappa-subtle); font-size: 0.75rem; }
</style>Controlled
Use v-model when application state must own the selected value.
<script setup>
import { ref } from "vue";
import { Radio } from "@dicehub/kappa/components/radio";
const visibility = ref("team");
</script>
<template>
<Radio.Root v-model="visibility" name="visibility">
<Radio.Label>Visibility</Radio.Label>
<Radio.Item value="private"><Radio.ItemControl /><Radio.ItemText>Private</Radio.ItemText></Radio.Item>
<Radio.Item value="team"><Radio.ItemControl /><Radio.ItemText>Team</Radio.ItemText></Radio.Item>
<Radio.Item value="public"><Radio.ItemControl /><Radio.ItemText>Public</Radio.ItemText></Radio.Item>
</Radio.Root>
<output aria-live="polite">Visibility: {{ visibility }}</output>
</template>Sliding Indicator
Add Indicator for a compact segmented choice. Ark UI measures and moves it to the selected item.
<script setup>
import { ref } from "vue";
import { Radio } from "@dicehub/kappa/components/radio";
const view = ref("board");
</script>
<template>
<Radio.Root v-model="view" aria-label="View" name="view" orientation="horizontal" class="view-switcher">
<Radio.Indicator />
<Radio.Item v-for="option in ['list', 'board', 'timeline']" :key="option" :value="option" class="view-switcher__item">
<Radio.ItemText>{{ option }}</Radio.ItemText>
</Radio.Item>
</Radio.Root>
</template>
<style scoped>
:deep(.view-switcher.kappa-radio) {
display: inline-grid;
grid-auto-columns: minmax(5.5rem, 1fr);
grid-auto-flow: column;
gap: 0;
border: 1px solid var(--kappa-line);
border-radius: 0.625rem;
padding: 0.25rem;
background: var(--kappa-overlay);
}
:deep(.view-switcher .kappa-radio__indicator) {
background: var(--kappa-control);
box-shadow: inset 0 0 0 1px var(--kappa-line), var(--kappa-shadow);
}
:deep(.view-switcher__item) {
width: 100%;
justify-content: center;
padding: 0.375rem 0.625rem;
}
</style>States
Disabled blocks the group, read-only preserves the value, and invalid needs clear error text.
Choose a delivery method.
<script setup>
import { Radio } from "@dicehub/kappa/components/radio";
</script>
<template>
<Radio.Root default-value="email" disabled name="disabled-channel">
<Radio.Label>Disabled group</Radio.Label>
<Radio.Item value="email"><Radio.ItemControl /><Radio.ItemText>Email</Radio.ItemText></Radio.Item>
<Radio.Item value="sms"><Radio.ItemControl /><Radio.ItemText>SMS</Radio.ItemText></Radio.Item>
</Radio.Root>
<Radio.Root default-value="automatic" read-only name="readonly-sync">
<Radio.Label>Read-only group</Radio.Label>
<Radio.Item value="automatic"><Radio.ItemControl /><Radio.ItemText>Automatic</Radio.ItemText></Radio.Item>
<Radio.Item value="manual"><Radio.ItemControl /><Radio.ItemText>Manual</Radio.ItemText></Radio.Item>
</Radio.Root>
<Radio.Root invalid required name="delivery" aria-describedby="delivery-error">
<Radio.Label>Delivery method</Radio.Label>
<Radio.Item value="standard"><Radio.ItemControl /><Radio.ItemText>Standard</Radio.ItemText></Radio.Item>
<Radio.Item value="express"><Radio.ItemControl /><Radio.ItemText>Express</Radio.ItemText></Radio.Item>
</Radio.Root>
<p id="delivery-error">Choose a delivery method.</p>
</template>Accessibility
- The root uses
role="radiogroup"and itsLabelprovides the accessible group name. - Tab enters the checked item, or the first enabled item when no value is selected.
- Arrow keys move focus and select the next or previous option. Space selects the focused option.
- Each item uses a native radio input for forms, validation, reset, and assistive technology.
- Use one clear group label. Put supporting or error text in
aria-describedby. - Do not use Radio for multiple selections. Use Checkbox instead.
- Focus remains visible on the item control. Reduced motion removes dot and indicator transitions.
API Reference
Radio.Root
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | null | - | Controlled selected value; supports v-model. |
defaultValue | string | null | null | Initial selected value for uncontrolled use. |
name | string | generated id | Shared native form name for every item input. |
orientation | "vertical" | "horizontal" | "vertical" | Sets layout and arrow-key movement. |
disabled | boolean | false | Disables every item in the group. |
readOnly | boolean | false | Keeps the selected value but blocks changes. |
required | boolean | false | Marks the native radio inputs as required. |
invalid | boolean | false | Marks the group and item controls invalid. |
form | string | - | Associates item inputs with an external form id. |
dir | "ltr" | "rtl" | inherited | Overrides the inherited direction used by keyboard navigation. |
id | string | generated | Stable identifier for the radio-group machine. |
ids | RadioRootProps['ids'] | generated | Overrides generated root, label, indicator, item, control, text, and input IDs. |
asChild | boolean | false | Merges root behavior into one custom child element. |
Radio.Item
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | required | Unique value selected and submitted by this item. |
disabled | boolean | false | Disables only this item. |
invalid | boolean | false | Marks only this item invalid. |
Parts
| Part | Element | Description |
|---|---|---|
Radio.Label | span | Accessible name for the radio group; clicking it focuses the group. |
Radio.Item | label | Selectable option that renders its native hidden input automatically. |
Radio.ItemControl | div | Visual circle with a default selected-state dot. |
Radio.ItemText | span | Accessible item label and composable text content. |
Radio.Indicator | div | Optional measured surface that follows the selected item. |
Radio.Context | renderless | Exposes the root radio-group API to its slot. |
Radio.ItemContext | renderless | Exposes checked, focused, hovered, active, disabled, and invalid item state. |
Radio.RootProvider | div | Root variant driven by an external useRadioGroup machine. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | null | Emitted when selection changes; drives v-model. |
valueChange | { value: string | null } | Ark UI detail emitted after selection changes. |
Exports
| Export | Description |
|---|---|
Radio | Compound API exposing Root, RootProvider, Label, Indicator, Item, ItemControl, ItemText, Context, and ItemContext. |
RadioRoot | Unaugmented group root with controlled and uncontrolled state. |
RadioRootProvider | Group root driven by an external useRadioGroup machine. |
RadioLabel | Accessible group label. |
RadioIndicator | Optional moving selection surface. |
RadioItem | Radio option with an automatic native hidden input. |
RadioItemControl | Visual radio control with a default dot. |
RadioItemText | Accessible option text. |
RadioContext | Renderless root context consumer. |
RadioItemContext | Renderless item-state context consumer. |
RadioProps | Public root prop contract. |
RadioItemProps | Public item prop contract. |
RadioGroupValueChangeDetails | Payload for valueChange. |
useRadioGroup | Ark UI machine hook for external state control. |
useRadioGroupContext | Ark UI root context hook. |
useRadioGroupItemContext | Ark UI item-state context hook. |
radioGroupAnatomy | Ark UI part anatomy metadata. |