Segmented Control
A Segmented control allows users to make a single selection from multiple exclusive options, providing a visually distinct and intuitive way of interacting with radio inputs.
Features
- Syncs with
disabled
state of fieldset - Syncs with form
reset
events - Can programmatically set segmented control value
- Can programmatically focus and blur segmented control items
Installation
To use segmented control add the radio machine to 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/vue # or yarn add @zag-js/radio-group @zag-js/vue
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/solid # or yarn add @zag-js/radio-group @zag-js/solid
This command will install the framework agnostic radio group logic and the reactive utilities for your framework of choice.
Anatomy
To set up the segmented control 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.
On a high level, the segmented control consists of:
- Root: The root container for the segmented control
- Indicator: The element that visually represents the active radio item.
- Radio: The root container for the each radio item
- Radio Label: The label that gives the user information on each radio item
- Radio Hidden Input: The native html input that is visually hidden in each radio item.
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 = [ { label: "React", value: "react" }, { label: "Angular", value: "ng" }, { label: "Vue", value: "vue" }, { label: "Svelte", value: "svelte" }, ] function Radio() { const [state, send] = useMachine(radio.machine({ id: "1" })) const api = radio.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <div {...api.indicatorProps} /> {items.map((opt) => ( <label key={opt.value} {...api.getItemProps({ value: opt.value })}> <span {...api.getItemTextProps({ value: opt.value })}> {opt.label} </span> <input {...api.getItemHiddenInputProps({ value: opt.value })} /> </label> ))} </div> ) }
import * as radio from "@zag-js/radio-group" import { normalizeProps, useMachine } from "@zag-js/vue" import { defineComponent, h, Fragment, computed } from "vue" const items = [ { label: "React", value: "react" }, { label: "Angular", value: "ng" }, { label: "Vue", value: "vue" }, { label: "Svelte", value: "svelte" }, ] export default defineComponent({ name: "Radio", setup() { const [state, send] = useMachine(radio.machine({ id: "radio" })) const apiRef = computed(() => radio.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.rootProps}> {items.map((opt) => ( <label {...api.getItemProps({ value: opt.value })}> <span {...api.getItemTextProps({ value: opt.value })}> {opt.label} </span> <input {...api.getItemHiddenInputProps({ value: opt.value })} /> </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 = [ { label: "React", value: "react" }, { label: "Angular", value: "ng" }, { label: "Vue", value: "vue" }, { label: "Svelte", value: "svelte" }, ]; const [state, send] = useMachine(radio.machine({ id: "1" })); const api = computed(() => radio.connect(state.value, send, normalizeProps)); </script> <template> <div v-bind="api.rootProps"> <div v-bind="api.indicatorProps" /> <div v-for="opt in items" :key="opt.value"> <label v-bind="api.getItemProps({ value: opt.value })"> <span v-bind="api.getItemTextProps({ value: opt.value })">{{ opt.label }}</span> <input v-bind="api.getItemHiddenInputProps({ value: opt.value })" /> </label> </div> </div> </template>
import * as radio from "@zag-js/radio-group" import { normalizeProps, useMachine } from "@zag-js/solid" import { Index, createMemo, createUniqueId } from "solid-js" const items = [ { label: "React", value: "react" }, { label: "Angular", value: "ng" }, { label: "Vue", value: "vue" }, { label: "Svelte", value: "svelte" }, ] function Radio() { const [state, send] = useMachine(radio.machine({ id: createUniqueId() })) const api = createMemo(() => radio.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> <div {...api().indicatorProps} /> <Index each={items}> {(item) => ( <label {...api().getItemProps({ value: item().value })}> <span {...api().getItemTextProps({ value: item().value })}> {item().label} </span> <input {...api().getItemHiddenInputProps({ value: item().value })} /> </label> )} </Index> </div> ) }
Disabling the segmented control
To make a segmented control disabled, set the context's disabled
property to
true
const [state, send] = useMachine( radio.machine({ disabled: true, }), )
Making the segmented control readonly
To make a segmented control readonly, set the context's readOnly
property to
true
const [state, send] = useMachine( radio.machine({ readOnly: true, }), )
Setting the initial value
To set the segmented control'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 segmented control value changes, the onValueChange
callback is
invoked.
const [state, send] = useMachine( radio.machine({ onValueChange(details) { // details => { value: string } console.log("segmented control value is:", details.value) }, }), )
Usage within forms
To use segmented control 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 segmented control part has a data-part
attribute added to them to select and style them in the DOM.
Indicator
Style the segmented control Indicator through the indicator
part.
[data-part="indicator"] { /* styles for indicator */ }
Focused State
When the radio input is focused, the data-focus
attribute is added to the root
and label parts.
[data-part="radio"][data-focus] { /* styles for radio 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
and label parts.
[data-part="radio"][data-disabled] { /* styles for radio 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 and
label parts.
[data-part="radio"][data-invalid] { /* styles for radio invalid state */ } [data-part="radio-label"][data-invalid] { /* styles for radio label invalid state */ }
Methods and Properties
The segmented control's api
provides properties and methods you can use to
programmatically read and set the segmented control's value.
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
Edit this page on GitHub