Skip to main content
Vuetify0 v1.0 is here
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

EmRadio

Edit this page
Report a Bug
Open issues
Copy Markdown

Renders elementIntermediateAug 14, 2026

A single-selection group of radios. EmRadioGroup owns the value; each EmRadio is one choice, with its label in the default slot and three sizes on the shared control scale.

Usage

The value lives on the group. v-model on EmRadioGroup holds the value of whichever EmRadio is checked, and each radio’s value is required — it is what group selection and form submission identify the choice by.

A radio’s label is its default slot. Leave the slot empty — an icon-adjacent radio, a table cell — and the control has no visible text, so pass label instead; an unlabelled radio is an unnamed control. Selection only ever moves, it never clears: clicking the checked radio again keeps it checked, which is the native radio contract.

<script setup lang="ts">
  import { EmRadio, EmRadioGroup } from '@paper/emerald'

  import { shallowRef } from 'vue'

  const plan = shallowRef('starter')
</script>

<template>
  <div class="emerald-docs-stack">
    <EmRadioGroup v-model="plan" label="Billing plan">
      <EmRadio value="starter">Starter</EmRadio>

      <EmRadio value="team">Team</EmRadio>

      <EmRadio value="enterprise">Enterprise</EmRadio>
    </EmRadioGroup>

    <p class="emerald-docs-note">Selected: {{ plan }}</p>
  </div>
</template>

<style>
  .emerald-docs-stack {
    display: flex;
    flex-direction: column;
    gap: var(--emerald-spacing-m, 16px);
  }

  .emerald-docs-note {
    margin: 0;
    font-family: var(--emerald-font-sans, Manrope, system-ui, sans-serif);
    font-size: var(--emerald-text-b3-size, 12px);
    line-height: var(--emerald-text-b3-height, 16px);
    color: var(--emerald-neutral-600, #939dac);
  }
</style>

Anatomy

vue
<script setup lang="ts">
  import { EmRadio, EmRadioGroup } from '@paper/emerald'
</script>

<template>
  <EmRadioGroup>
    <EmRadio />
  </EmRadioGroup>
</template>

Composed on v0

EmRadioGroup renders v0’s Radio compound — Radio.Group directly, while each EmRadio wraps Radio.Root with a Radio.Indicator inside. All of the behavior is v0’s: Radio.Group runs the single-selection registry and the v-model bridge, and Radio.Root owns the roving tabindex, the arrow-key handling and the role="radio" semantics. Emerald adds the two spans that make it look like a radio — the circle behind, the dot inside the indicator — and the stylesheet.

The split shows in the markup. Radio.Root renders a <button>, and a wrapping <label> does not name a button the way it names an <input> — so EmRadio renders the label text in a sibling span and points the button at it with aria-labelledby. The wrapping .emerald-radio label still exists, but only so clicking the text activates the control; the accessible name travels by reference.

State never travels through classes. Radio.Root publishes data-state="checked" | "unchecked" and data-disabled, Radio.Indicator hides itself while unchecked, and Emerald’s stylesheet hangs every checked, hover and disabled rule off those attributes. The wrapper adds two attributes of its own — data-size for the scale and data-disabled mirroring the radio’s own disabled prop — and those are Emerald’s, not v0’s.

Form participation is v0’s too. Give EmRadioGroup a name and it flows through context to every radio, and Radio.Root renders v0’s Radio.HiddenInput — an inert, visually hidden native <input type="radio"> kept in sync with the checked state — so the group submits with a plain <form> without any of the visible controls being inputs. Object values are JSON-serialized for submission.

Examples

Sizes

size sits on each EmRadio rather than on the group, and moves the control across the same scale the checkbox uses: sm is 16px, md — the default — is 20px, and lg is 24px, with the dot stepping along with the circle. The label text does not change size; only the control does.

Set the same size on every radio in a group. The prop is per-radio only because the radio owns its own control box, not because mixed sizes within one group are a supported layout — a group with three control sizes reads as three different questions.

Pick by the surface’s density, the same way you would for EmButton: sm inside tables and dense settings panels, md for ordinary forms, lg where the choice is the page’s main event and the target needs to be generous.

Disabled radios and disabled groups

disabled exists at both levels and means the same thing at different scopes. On one EmRadio it withdraws a single option — it stays visible, dims, and cannot be selected by pointer or keyboard; the arrow keys skip straight over it. On EmRadioGroup it freezes the whole question: every radio dims and the group drops out of the tab order entirely.

Neither is the native disabled attribute. The radio is a <button> that v0 marks with aria-disabled and tabindex="-1" and whose activation is guarded in the handler, so assistive technology still perceives the option and announces it as unavailable — which is the point of showing a disabled choice at all. A choice that should not be perceived should be removed, not disabled.

One asymmetry to know: a radio disabled by its own prop dims both its control and its label text, while a group-wide disable dims the controls only. When the reason for the lock matters, put it in the group’s name rather than relying on the dimming — this example bakes it into the label prop, which screen readers announce; to show the same words to sighted readers, render a visible heading and point ariaLabelledby at it instead.

Props

EmRadioGroup

PropTypeDefaultDescription
v-modelunknownValue of the checked radio
disabledbooleanfalseDisables every radio and removes the group from the tab order
namestringForm field name, shared with every radio. Each radio renders a hidden native input when set
mandatoryboolean | 'force'false'force' auto-selects the first non-disabled radio on mount. Selection can never be cleared through the UI either way, so 'force' is the meaningful option
labelstringAccessible name for the radiogroup when no visible label element is associated
ariaLabelledbystringID of an existing element that labels the radiogroup
namespacestringWhich v0 Radio.Group context to provide. Only needed when nesting

The default slot holds the radios. There are no named slots.

EmRadio

PropTypeDefaultDescription
valueunknownRequired. Value this radio contributes to the group model and to form submission
disabledbooleanfalseWithdraws this option — unselectable, skipped by arrow keys, removed from the tab order
size'sm' | 'md' | 'lg''md'Control size on the shared checkbox scale — 16, 20 or 24px
labelstringAccessible name when the default slot is empty
namespacestringWhich v0 Radio.Root context this radio provides. Only needed when nesting
groupNamespacestringWhich v0 Radio.Group this radio registers with. Only needed when nesting

The default slot is the visible label. There are no named slots.

Accessibility

Radio.Group renders role="radiogroup" and Radio.Root renders a <button role="radio"> with aria-checked, so the group announces as a set of exclusive choices with one of them selected.

Naming

Name the group. label becomes aria-label; ariaLabelledby points at a heading or legend you already render, and wins when both are set. A radiogroup without a name announces as an anonymous cluster of options with no question attached.

Each radio is named by its slot text — via aria-labelledby to the sibling span, since a wrapping label does not name a button — and by the label prop when the slot is empty. The circle and dot are aria-hidden; they contribute nothing to the name.

Keyboard

The group is a single tab stop with a roving tabindex. Tab lands on the checked radio, or on the first enabled radio when nothing is checked yet; a fully disabled group is skipped entirely.

KeyBehavior
Tab / Shift + TabInto the group at the checked radio, then out — never between radios
Arrow Down / Arrow RightFocus and select the next enabled radio, wrapping from last to first
Arrow Up / Arrow LeftFocus and select the previous enabled radio, wrapping from first to last
Enter, SpaceSelect the focused radio

Selection follows focus — arrowing through the group selects as it moves, which is the APG default for radio groups. v0’s Radio.Group has a manual activation mode, but EmRadioGroup does not expose it, so the automatic behavior is the only one here. Disabled radios are skipped by the arrow keys, not landed on.

Enter selecting is a small extension over native radios, which only respond to Space; since both keys are handled explicitly, nothing double-fires.

Disabled

Disabled radios use aria-disabled with tabindex="-1" rather than the native disabled attribute — perceivable and announced as dimmed, unreachable by Tab, activation guarded. Group-level disabled applies the same treatment to every radio at once, which removes the whole group from keyboard reach; when the reason for the lock matters, put it in the group’s name — the label prop for what is announced, or a visible element referenced by ariaLabelledby so sighted readers get the same words.

Focus

The focus ring is an outline on the circle, and it only appears for :focus-visible — keyboard focus draws it, a pointer click does not leave a ring behind.

Was this page helpful?

© 2016-1970 Vuetify, LLC
Services
Ctrl+/