Skip to content
Kappa
Radio
@dicehub/kappav0.4.2

Radio

Selects one option from a mutually exclusive set.

Interface density
Selected: comfortable
<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.

Notification channel
<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

Typical part hierarchy
Radio.Root
├── Radio.Label
├── Radio.Indicator (optional)
├── Radio.Item
│   ├── HiddenInput (automatic)
│   ├── Radio.ItemControl
│   └── Radio.ItemText
└── Radio.Item
    ├── HiddenInput (automatic)
    ├── Radio.ItemControl
    └── Radio.ItemText

Ark 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.

  • Root owns one selected string or null.
  • Label names the group and moves focus into it when clicked.
  • Item stays a semantic label and always renders its native input.
  • ItemControl renders a default dot; its slot can replace that mark.
  • ItemText supplies the accessible option name and can hold rich text.
  • Indicator is 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.

Theme
<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.

Backup frequency
<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.

Sort order
<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.

Storage plan
<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.

Visibility
Visibility: team
<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.

Disabled group
Read-only group
Delivery method

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 its Label provides 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

PropTypeDefaultDescription
modelValuestring | null-Controlled selected value; supports v-model.
defaultValuestring | nullnullInitial selected value for uncontrolled use.
namestringgenerated idShared native form name for every item input.
orientation"vertical" | "horizontal""vertical"Sets layout and arrow-key movement.
disabledbooleanfalseDisables every item in the group.
readOnlybooleanfalseKeeps the selected value but blocks changes.
requiredbooleanfalseMarks the native radio inputs as required.
invalidbooleanfalseMarks the group and item controls invalid.
formstring-Associates item inputs with an external form id.
dir"ltr" | "rtl"inheritedOverrides the inherited direction used by keyboard navigation.
idstringgeneratedStable identifier for the radio-group machine.
idsRadioRootProps['ids']generatedOverrides generated root, label, indicator, item, control, text, and input IDs.
asChildbooleanfalseMerges root behavior into one custom child element.

Radio.Item

PropTypeDefaultDescription
valuestringrequiredUnique value selected and submitted by this item.
disabledbooleanfalseDisables only this item.
invalidbooleanfalseMarks only this item invalid.

Parts

PartElementDescription
Radio.LabelspanAccessible name for the radio group; clicking it focuses the group.
Radio.ItemlabelSelectable option that renders its native hidden input automatically.
Radio.ItemControldivVisual circle with a default selected-state dot.
Radio.ItemTextspanAccessible item label and composable text content.
Radio.IndicatordivOptional measured surface that follows the selected item.
Radio.ContextrenderlessExposes the root radio-group API to its slot.
Radio.ItemContextrenderlessExposes checked, focused, hovered, active, disabled, and invalid item state.
Radio.RootProviderdivRoot variant driven by an external useRadioGroup machine.

Events

EventPayloadDescription
update:modelValuestring | nullEmitted when selection changes; drives v-model.
valueChange{ value: string | null }Ark UI detail emitted after selection changes.

Exports

ExportDescription
RadioCompound API exposing Root, RootProvider, Label, Indicator, Item, ItemControl, ItemText, Context, and ItemContext.
RadioRootUnaugmented group root with controlled and uncontrolled state.
RadioRootProviderGroup root driven by an external useRadioGroup machine.
RadioLabelAccessible group label.
RadioIndicatorOptional moving selection surface.
RadioItemRadio option with an automatic native hidden input.
RadioItemControlVisual radio control with a default dot.
RadioItemTextAccessible option text.
RadioContextRenderless root context consumer.
RadioItemContextRenderless item-state context consumer.
RadioPropsPublic root prop contract.
RadioItemPropsPublic item prop contract.
RadioGroupValueChangeDetailsPayload for valueChange.
useRadioGroupArk UI machine hook for external state control.
useRadioGroupContextArk UI root context hook.
useRadioGroupItemContextArk UI item-state context hook.
radioGroupAnatomyArk UI part anatomy metadata.