Radio Group
A radio group allows users to make a single choice from a select number of option
Fruits
Features
- Syncs with
disabled
state of fieldset - Syncs with form
reset
events - Can programmatically set radio group value
- Can programmatically focus and blur radio items
Installation
To use the radio machine in your project, run the following command in your command line:
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
This command will install the framework agnostic radio group logic and the reactive utilities for your framework of choice.
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-part
attribute to help identify them in the DOM.
Usage
First, import the radio group package into your project
import * as radio from "@zag-js/radio-group"
The radio package exports two key functions:
machine
— The state machine logic for the radio widget.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll also need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the radio machine in your project 🔥
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 [state, send] = useMachine(radio.machine({ id: "1" })) const api = radio.connect(state, send, 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 [state, send] = useMachine(radio.machine({ id: createUniqueId() })) const api = createMemo(() => radio.connect(state, send, 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 [state, send] = useMachine(radio.machine({ id: "1" })) const api = computed(() => radio.connect(state.value, send, 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 [snapshot, send] = useMachine(radio.machine({ id: "1", name: "fruit" })) const api = $derived(radio.connect(snapshot, send, 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
To make a radio group disabled, set the context's disabled
property to true
const [state, send] = useMachine( radio.machine({ disabled: true, }), )
Setting the initial value
To set the radio group's initial value, set the context's value
property to
the value of the radio item to be selected by default
const [state, send] = useMachine( radio.machine({ value: "apple", }), )
Listening for changes
When the radio group value changes, the onValueChange
callback is invoked.
const [state, send] = useMachine( radio.machine({ onValueChange(details) { // details => { value: string } console.log("radio value is:", details.value) }, }), )
Usage within forms
To use radio group within forms, use the exposed inputProps
from the connect
function and ensure you pass name
value to the machine's context. It will
render a hidden input and ensure the value changes get propagated to the form
correctly.
const [state, send] = useMachine( radio.machine({ name: "fruits", }), )
Styling guide
Earlier, we mentioned that each radio part has a data-part
attribute added to
them to select and style them in the DOM.
Checked State
When the radio input is checked, the data-state
attribute is added to the
[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:
ids
Partial<{ 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.value
string
The value of the checked radioname
string
The name of the input fields in the radio (Useful for form submission).form
string
The associate form of the underlying input.disabled
boolean
If `true`, the radio group will be disabledreadOnly
boolean
Whether the checkbox is read-onlyonValueChange
(details: ValueChangeDetails) => void
Function called once a radio is checkedorientation
"horizontal" | "vertical"
Orientation of the radio groupdir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The radio group api
exposes the following methods:
value
string
The current value of the radio groupsetValue
(value: string) => void
Function to set the value of the radio groupclearValue
() => void
Function to clear the value of the radio groupfocus
() => void
Function to focus the radio groupgetItemState
(props: ItemProps) => ItemState
Returns 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.
Edit this page on GitHub