Radio Group
A radio group lets users select one option from a set.
Features
- Syncs with
disabledstate of fieldset - Syncs with form
resetevents - Can programmatically set radio group value
- Can programmatically focus and blur radio items
Installation
Install the radio package:
npm install @zag-js/radio-group @zag-js/react # or yarn add @zag-js/radio-group @zag-js/react
npm install @zag-js/radio-group @zag-js/solid # or yarn add @zag-js/radio-group @zag-js/solid
npm install @zag-js/radio-group @zag-js/vue # or yarn add @zag-js/radio-group @zag-js/vue
npm install @zag-js/radio-group @zag-js/svelte # or yarn add @zag-js/radio-group @zag-js/svelte
Anatomy
To set up the radio group correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the radio group package:
import * as radio from "@zag-js/radio-group"
The radio package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Pass a unique
idtouseMachineso generated element ids stay predictable.
Then use the framework integration helpers:
import * as radio from "@zag-js/radio-group" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" const items = [ { id: "apple", label: "Apples" }, { id: "orange", label: "Oranges" }, { id: "mango", label: "Mangoes" }, { id: "grape", label: "Grapes" }, ] function Radio() { const service = useMachine(radio.machine, { id: useId() }) const api = radio.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <h3 {...api.getLabelProps()}>Fruits</h3> {items.map((opt) => ( <label key={opt.id} {...api.getItemProps({ value: opt.id })}> <span {...api.getItemTextProps({ value: opt.id })}>{opt.label}</span> <input {...api.getItemHiddenInputProps({ value: opt.id })} /> <div {...api.getItemControlProps({ value: opt.id })} /> </label> ))} </div> ) }
import * as radio from "@zag-js/radio-group" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" const items = [ { id: "apple", label: "Apples" }, { id: "orange", label: "Oranges" }, { id: "mango", label: "Mangoes" }, { id: "grape", label: "Grapes" }, ] function Radio() { const service = useMachine(radio.machine, { id: createUniqueId() }) const api = createMemo(() => radio.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <h3 {...api().getLabelProps()}>Fruits</h3> {items.map((opt) => ( <label {...api().getItemProps({ value: opt.id })}> <span {...api().getItemTextProps({ value: opt.id })}> {opt.label} </span> <input {...api().getItemHiddenInputProps({ value: opt.id })} /> <div {...api().getItemControlProps({ value: opt.id })} /> </label> ))} </div> ) }
<script setup> import * as radio from "@zag-js/radio-group" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" const items = [ { id: "apple", label: "Apples" }, { id: "orange", label: "Oranges" }, { id: "mango", label: "Mangoes" }, { id: "grape", label: "Grapes" }, ] const service = useMachine(radio.machine, { id: "1" }) const api = computed(() => radio.connect(service, normalizeProps)) </script> <template> <div v-bind="api.getRootProps()"> <h3 v-bind="api.getLabelProps()">Fruits</h3> <div v-for="opt in items" :key="opt.id"> <label v-bind="api.getItemProps({ value: opt.id })"> <span v-bind="api.getItemTextProps({ value: opt.id })" >{{ opt.label }}</span > <input v-bind="api.getItemHiddenInputProps({ value: opt.id })" /> <div v-bind="api.getItemControlProps({ value: opt.id })" /> </label> </div> </div> </template>
<script lang="ts"> import * as radio from "@zag-js/radio-group" import { useMachine, normalizeProps } from "@zag-js/svelte" const items = [ { id: "apple", label: "Apples" }, { id: "orange", label: "Oranges" }, { id: "mango", label: "Mangoes" }, { id: "grape", label: "Grapes" }, ] const id = $props.id() const service = useMachine(radio.machine, { id, name: "fruit", }) const api = $derived(radio.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <h3 {...api.getLabelProps()}>Fruits</h3> {#each items as opt} <label {...api.getItemProps({ value: opt.id })}> <span {...api.getItemTextProps({ value: opt.id })}>{opt.label}</span> <input {...api.getItemHiddenInputProps({ value: opt.id })} /> <div {...api.getItemControlProps({ value: opt.id })}></div> </label> {/each} </div>
Disabling the radio group
Set disabled to true to disable all radio items.
const service = useMachine(radio.machine, { disabled: true, })
Setting the initial value
Use the defaultValue property to set the radio group's initial value.
const service = useMachine(radio.machine, { defaultValue: "apple", })
Controlled value
Use value and onValueChange to control selection externally.
const service = useMachine(radio.machine, { value, onValueChange(details) { setValue(details.value) }, })
Listening for changes
When the radio group value changes, the onValueChange callback is invoked.
const service = useMachine(radio.machine, { onValueChange(details) { // details => { value: string | null } console.log("radio value is:", details.value) }, })
Usage within forms
To use radio group in forms, set name.
const service = useMachine(radio.machine, { name: "fruits", })
Set form if the radio inputs should submit with a form outside the current DOM
subtree.
const service = useMachine(radio.machine, { name: "fruits", form: "checkout-form", })
Vertical orientation
Set orientation when you need a vertical layout.
const service = useMachine(radio.machine, { orientation: "vertical", })
Read-only and required state
Use readOnly and required to control form behavior.
const service = useMachine(radio.machine, { readOnly: true, required: true, })
Invalid state
Set invalid to style and expose invalid form state.
const service = useMachine(radio.machine, { invalid: true, })
Styling guide
Each radio part includes a data-part attribute you can target in CSS.
Checked State
When the radio input is checked, the data-state attribute is added to the
item, control, and label parts.
[data-part="radio"][data-state="checked|unchecked"] { /* styles for radio checked or unchecked state */ } [data-part="radio-control"][data-state="checked|unchecked"] { /* styles for radio checked or unchecked state */ } [data-part="radio-label"][data-state="checked|unchecked"] { /* styles for radio checked or unchecked state */ }
Focused State
When the radio input is focused, the data-focus attribute is added to the
root, control and label parts.
[data-part="radio"][data-focus] { /* styles for radio focus state */ } [data-part="radio-control"][data-focus] { /* styles for radio control focus state */ } [data-part="radio-label"][data-focus] { /* styles for radio label focus state */ }
Disabled State
When the radio is disabled, the data-disabled attribute is added to the root,
control and label parts.
[data-part="radio"][data-disabled] { /* styles for radio disabled state */ } [data-part="radio-control"][data-disabled] { /* styles for radio control disabled state */ } [data-part="radio-label"][data-disabled] { /* styles for radio label disabled state */ }
Invalid State
When the radio is invalid, the data-invalid attribute is added to the root,
control and label parts.
[data-part="radio"][data-invalid] { /* styles for radio invalid state */ } [data-part="radio-control"][data-invalid] { /* styles for radio control invalid state */ } [data-part="radio-label"][data-invalid] { /* styles for radio label invalid state */ }
Methods and Properties
Machine Context
The radio group machine exposes the following context properties:
idsPartial<{ root: string; label: string; indicator: string; item: (value: string) => string; itemLabel: (value: string) => string; itemControl: (value: string) => string; itemHiddenInput: (value: string) => string; }> | undefinedThe ids of the elements in the radio. Useful for composition.valuestring | null | undefinedThe controlled value of the radio groupdefaultValuestring | null | undefinedThe initial value of the checked radio when rendered. Use when you don't need to control the value of the radio group.namestring | undefinedThe name of the input fields in the radio (Useful for form submission).formstring | undefinedThe associate form of the underlying input.disabledboolean | undefinedIf `true`, the radio group will be disabledinvalidboolean | undefinedIf `true`, the radio group is marked as invalid.requiredboolean | undefinedIf `true`, the radio group is marked as required.readOnlyboolean | undefinedWhether the radio group is read-onlyonValueChange((details: ValueChangeDetails) => void) | undefinedFunction called once a radio is checkedorientation"horizontal" | "vertical" | undefinedOrientation of the radio groupdir"ltr" | "rtl" | undefinedThe document's text/writing direction.idstringThe unique identifier of the machine.getRootNode(() => ShadowRoot | Node | Document) | undefinedA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The radio group api exposes the following methods:
valuestring | nullThe current value of the radio groupsetValue(value: string) => voidFunction to set the value of the radio groupclearValueVoidFunctionFunction to clear the value of the radio groupfocusVoidFunctionFunction to focus the radio groupgetItemState(props: ItemProps) => ItemStateReturns the state details of a radio input
Accessibility
Adheres to the Radio Group WAI-ARIA design pattern
Keyboard Interactions
- TabMoves focus to either the checked radio item or the first radio item in the group.
- SpaceWhen focus is on an unchecked radio item, checks it.
- ArrowDownMoves focus and checks the next radio item in the group.
- ArrowRightMoves focus and checks the next radio item in the group.
- ArrowUpMoves focus to the previous radio item in the group.
- ArrowLeftMoves focus to the previous radio item in the group.