Frappe UIFrappe UI

Radio

Lets users pick a single option from a set. RadioGroup owns the selected value; each Radio inside it declares the value it represents.

vue
<RadioGroup v-model="plan" label="Choose a plan">
  <Radio value="free" label="Free" />
  <Radio value="pro" label="Pro" />
</RadioGroup>

Radio must be used inside a RadioGroup — it throws otherwise. The group handles arrow-key navigation, roving focus and form submission, so options only need a value.

Playground

label
description
size
orientation
padded
required
error
disabled
<RadioGroup
  v-model="value"
  label="Notify me by"
>
  <Radio value="email" label="Email" />
  <Radio value="sms" label="SMS" />
</RadioGroup>

Required

The asterisk belongs on the group heading, not on each option — a radio is one choice within a group, so marking every option would be wrong. Set required on RadioGroup and it renders on the heading.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Radio, RadioGroup } from 'frappe-ui'

const plan = ref()
</script>

<template>
  <RadioGroup v-model="plan" label="Choose a plan" required class="w-96">
    <Radio value="free" label="Free" />
    <Radio value="pro" label="Pro" />
    <Radio value="enterprise" label="Enterprise" />
  </RadioGroup>
</template>

States

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Radio, RadioGroup } from 'frappe-ui'

// Each state needs its own group — within one group only a single option can
// be selected, which is the whole point of a radio.
const unselected = ref('')
const selected = ref('on')
const disabled = ref('')
const disabledSelected = ref('on')
</script>

<template>
  <div class="flex flex-col gap-3 items-start">
    <RadioGroup v-model="unselected">
      <Radio value="on" label="Unselected" />
    </RadioGroup>
    <RadioGroup v-model="selected">
      <Radio value="on" label="Selected" />
    </RadioGroup>
    <RadioGroup v-model="disabled" disabled>
      <Radio value="on" label="Disabled" />
    </RadioGroup>
    <RadioGroup v-model="disabledSelected" disabled>
      <Radio value="on" label="Disabled selected" />
    </RadioGroup>
  </div>
</template>

Settings list

Use padded for mutually exclusive settings. A description stacks below the label, and the whole row is the click target.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { Radio, RadioGroup } from 'frappe-ui'

const notify = ref('all')
</script>

<template>
  <RadioGroup v-model="notify" padded class="w-96">
    <Radio value="all" label="All new emails" />
    <Radio
      value="important"
      label="Important emails only"
      description="Only emails where you're mentioned or assigned."
    />
    <Radio value="off" label="Mail notifications off" disabled />
  </RadioGroup>
</template>

API Reference

Radio

Show types
typescript
import type { ComputedRef, InjectionKey } from 'vue'
import type { ToggleSize } from '../../composables/inputTypes'
import type { InputLabelingProps } from '../../composables/useInputLabeling'

/** The value a radio represents within its group. */
export type RadioValue = string | number | boolean

export interface RadioGroupProps extends InputLabelingProps {
  /** The selected value of the group. */
  modelValue?: RadioValue

  /** Size of every radio in the group. */
  size?: ToggleSize

  /** Wraps each option in a clickable surface with hover, active and focus states — useful for selection lists and menu items. */
  padded?: boolean

  /** Disables every radio in the group. */
  disabled?: boolean

  /** Layout of the options. Also decides which arrow keys move the selection: up/down when vertical, left/right when horizontal. */
  orientation?: 'vertical' | 'horizontal'

  /** When `true`, arrow-key navigation wraps from the last option back to the first. */
  loop?: boolean

  /** Native `name` for the hidden form input, so the group submits with a form. Auto-generated when omitted. */
  name?: string
}

export interface RadioGroupEmits {
  /** Fired when the selected value changes. */
  'update:modelValue': [value: RadioValue]
}

// `required` and `error` are intentionally omitted: a radio is one option within
// a group, so the asterisk and the error message belong on the group, not on
// each option. Set them on `<RadioGroup>` instead.
//
// `size` and `padded` are likewise group-level — mixing sizes within one group
// has no design meaning, so they are inherited rather than per-option.
export interface RadioProps extends Omit<
  InputLabelingProps,
  'required' | 'error'
> {
  /** The value this option represents within its group. */
  value: RadioValue

  /** Disables this option only. The group's `disabled` still wins when set. */
  disabled?: boolean
}

/** Group-level state that each `Radio` reads instead of taking as its own prop. */
export interface RadioGroupContext {
  size: ComputedRef<ToggleSize>
  padded: ComputedRef<boolean>
  disabled: ComputedRef<boolean>
}

export const RadioGroupContextKey: InjectionKey<RadioGroupContext> =
  Symbol('RadioGroupContext')
value*
RadioValue

The value this option represents within its group.

disabled
= false
boolean

Disables this option only. The group's `disabled` still wins when set.

label
string

Label rendered above (or beside, for binary controls) the input.

description
string

Helper text rendered below the input. Hidden when `error` is set.

id
string

HTML id of the underlying control. Auto-generated via `useId()` if omitted.

label

Overrides the rendered label content.

description

Overrides the rendered description content.

RadioGroup

Show types
typescript
import type { ComputedRef, InjectionKey } from 'vue'
import type { ToggleSize } from '../../composables/inputTypes'
import type { InputLabelingProps } from '../../composables/useInputLabeling'

/** The value a radio represents within its group. */
export type RadioValue = string | number | boolean

export interface RadioGroupProps extends InputLabelingProps {
  /** The selected value of the group. */
  modelValue?: RadioValue

  /** Size of every radio in the group. */
  size?: ToggleSize

  /** Wraps each option in a clickable surface with hover, active and focus states — useful for selection lists and menu items. */
  padded?: boolean

  /** Disables every radio in the group. */
  disabled?: boolean

  /** Layout of the options. Also decides which arrow keys move the selection: up/down when vertical, left/right when horizontal. */
  orientation?: 'vertical' | 'horizontal'

  /** When `true`, arrow-key navigation wraps from the last option back to the first. */
  loop?: boolean

  /** Native `name` for the hidden form input, so the group submits with a form. Auto-generated when omitted. */
  name?: string
}

export interface RadioGroupEmits {
  /** Fired when the selected value changes. */
  'update:modelValue': [value: RadioValue]
}

// `required` and `error` are intentionally omitted: a radio is one option within
// a group, so the asterisk and the error message belong on the group, not on
// each option. Set them on `<RadioGroup>` instead.
//
// `size` and `padded` are likewise group-level — mixing sizes within one group
// has no design meaning, so they are inherited rather than per-option.
export interface RadioProps extends Omit<
  InputLabelingProps,
  'required' | 'error'
> {
  /** The value this option represents within its group. */
  value: RadioValue

  /** Disables this option only. The group's `disabled` still wins when set. */
  disabled?: boolean
}

/** Group-level state that each `Radio` reads instead of taking as its own prop. */
export interface RadioGroupContext {
  size: ComputedRef<ToggleSize>
  padded: ComputedRef<boolean>
  disabled: ComputedRef<boolean>
}

export const RadioGroupContextKey: InjectionKey<RadioGroupContext> =
  Symbol('RadioGroupContext')
modelValue
RadioValue

The selected value of the group.

size
= "sm"
ToggleSize

Size of every radio in the group.

padded
= false
boolean

Wraps each option in a clickable surface with hover, active and focus states — useful for selection lists and menu items.

disabled
= false
boolean

Disables every radio in the group.

orientation
= "vertical"
"vertical" | "horizontal"

Layout of the options. Also decides which arrow keys move the selection: up/down when vertical, left/right when horizontal.

loop
= true
boolean

When `true`, arrow-key navigation wraps from the last option back to the first.

name
string

Native `name` for the hidden form input, so the group submits with a form. Auto-generated when omitted.

label
string

Label rendered above (or beside, for binary controls) the input.

description
string

Helper text rendered below the input. Hidden when `error` is set.

error
string | FrappeUIError

Error message rendered below the input. When set, the control receives `aria-invalid="true"` and `data-state="invalid"`. May be either a string or an `Error` object whose `messages?: string[]` is rendered as stacked lines (with `Error.message` as the fallback).

required
boolean

Marks the field as required. Renders an asterisk next to the label and forwards `required` / `aria-required` to the underlying control.

id
string

HTML id of the underlying control. Auto-generated via `useId()` if omitted.

default

The `<Radio>` options.

label
{ required: boolean; }

Overrides the rendered group heading. Receives `{ required }`.

description

Overrides the rendered group description.

update:modelValue
[value: RadioValue | undefined]

Fired when the model value changes.