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" 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: "1" }) 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; }>The ids of the elements in the radio. Useful for composition.valuestringThe controlled value of the radio groupdefaultValuestringThe initial value of the checked radio when rendered. Use when you don't need to control the value of the radio group.namestringThe name of the input fields in the radio (Useful for form submission).formstringThe associate form of the underlying input.disabledbooleanIf `true`, the radio group will be disabledinvalidbooleanIf `true`, the radio group is marked as invalid.requiredbooleanIf `true`, the radio group is marked as required.readOnlybooleanWhether the radio group is read-onlyonValueChange(details: ValueChangeDetails) => voidFunction called once a radio is checkedorientation"horizontal" | "vertical"Orientation of the radio groupdir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The radio group api exposes the following methods:
valuestringThe 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.