Beta, expect API change, and bugs. Hit one? Tell us.

Skip to content

Radio

One choice from a set of options.

Difficulty
<script setup lang="ts">
import { useId } from 'vue'
import { Radio } from '@shardsui/vue/radio'
import { RadioGroup } from '@shardsui/vue/radio-group'

const id = useId()

const levels = [
  { value: 'beginner', label: 'Beginner' },
  { value: 'intermediate', label: 'Intermediate' },
  { value: 'advanced', label: 'Advanced' }
]
</script>

<template>
  <RadioGroup
    value="intermediate"
    class="flex flex-col items-start gap-1 text-gray-900"
    :aria-labelledby="id"
  >
    <div class="text-sm font-semibold" :id="id">Difficulty</div>

    <label
      v-for="level in levels"
      :key="level.value"
      class="flex items-center gap-2 text-sm font-normal"
    >
      <Radio.Root
        :value="level.value"
        class="flex size-4 shrink-0 items-center justify-center rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-950 data-checked:bg-gray-900 data-unchecked:border data-unchecked:border-gray-300"
      >
        <Radio.Indicator class="flex before:size-2 before:rounded-full before:bg-gray-50" />
      </Radio.Root>
      {{ level.label }}
    </label>
  </RadioGroup>
</template>

Anatomy

A Radio only works inside a Radio Group. Import both and nest the radios inside the group:

<script setup>
import { Radio } from '@shardsui/vue/radio'
import { RadioGroup } from '@shardsui/vue/radio-group'
</script>

<template>
  <RadioGroup>
    <Radio.Root value="...">
      <Radio.Indicator />
    </Radio.Root>
  </RadioGroup>
</template>

Usage guidelines

  • Form controls must have an accessible name: name the group and each radio with <label> elements, or with the Field and Fieldset components. See Labeling a radio group.

Examples

Labeling a radio group

Point the group at a sibling label with aria-labelledby:

<template>
  <div id="storage-type-label">Storage type</div>
  <RadioGroup aria-labelledby="storage-type-label">
    <!-- radios -->
  </RadioGroup>
</template>

For each radio, an enclosing <label> takes the least markup. Radio.Root renders a <span> by default, so the label toggles the hidden <input type="radio"> natively:

<template>
  <label>
    <Radio.Root value="ssd">
      <Radio.Indicator />
    </Radio.Root>
    SSD
  </label>
</template>

Rendering as a native button

When each radio has its own label tied to it with for/id, render it as a native button with as="button". The id then lands on the button rather than the hidden input, so the label points at the focusable element:

<template>
  <div id="storage-type">Storage type</div>
  <RadioGroup value="ssd" aria-labelledby="storage-type">
    <div>
      <label for="storage-type-ssd">SSD</label>
      <Radio.Root value="ssd" id="storage-type-ssd" as="button">
        <Radio.Indicator />
      </Radio.Root>
    </div>
  </RadioGroup>
</template>

Form integration

Field.Root names the group and wires it into the form; Fieldset.Legend labels it. Give each radio its own Field.Item, which scopes the enclosed Field.Label to that radio alone:

<template>
  <Form>
    <Field.Root name="storageType">
      <Fieldset.Root>
        <Fieldset.Legend>Storage type</Fieldset.Legend>
        <RadioGroup>
          <Field.Item>
            <Field.Label>
              <Radio.Root value="ssd"><Radio.Indicator /></Radio.Root>
              SSD
            </Field.Label>
          </Field.Item>
          <Field.Item>
            <Field.Label>
              <Radio.Root value="hdd"><Radio.Indicator /></Radio.Root>
              HDD
            </Field.Label>
          </Field.Item>
        </RadioGroup>
      </Fieldset.Root>
    </Field.Root>
  </Form>
</template>

API reference

RadioGroup

Provides a shared state to a series of radio buttons. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-disabledPresent when the group is disabled.
data-readonlyPresent when the group is read-only.
data-requiredPresent when the group is required.
data-validPresent when the field is valid (when wrapped in Field.Root).
data-invalidPresent when the field is invalid (when wrapped in Field.Root).
data-touchedPresent when the field has been touched (when wrapped in Field.Root).
data-dirtyPresent when the value has changed (when wrapped in Field.Root).
data-filledPresent when the group has a selection (when wrapped in Field.Root).
data-focusedPresent when focus is inside the group (when wrapped in Field.Root).

Root

Represents the radio button itself. Renders a <span> element and a hidden <input> beside.

PropTypeDefault
AttributeDescription
data-checkedPresent when selected.
data-uncheckedPresent when not selected.
data-disabledPresent when disabled.
data-readonlyPresent when read-only.
data-requiredPresent when required.
data-validPresent when the field is valid (when wrapped in Field.Root).
data-invalidPresent when the field is invalid (when wrapped in Field.Root).
data-touchedPresent when the field has been touched (when wrapped in Field.Root).
data-dirtyPresent when the value has changed (when wrapped in Field.Root).
data-filledPresent when the group has a selection (when wrapped in Field.Root).
data-focusedPresent when focus is inside the group (when wrapped in Field.Root).

Indicator

Indicates whether the radio button is selected. Renders a <span> element.

PropTypeDefault

Inherits the same data attributes as Root, plus:

AttributeDescription
data-starting-stylePresent when the indicator is animating in.
data-ending-stylePresent when the indicator is animating out.