Toggle Group
A set of toggle buttons that share one state, for picking one option or several from a short list.
<ToggleGroup
variant="segmented"
size="sm"
defaultValue={["7d"]}
aria-label="Time range"
>
<ToggleGroupItem value="24h">24h</ToggleGroupItem>
<ToggleGroupItem value="7d">7 days</ToggleGroupItem>
<ToggleGroupItem value="30d">30 days</ToggleGroupItem>
<ToggleGroupItem value="90d">90 days</ToggleGroupItem>
</ToggleGroup>Usage
import {
ToggleGroup,
ToggleGroupItem,
} from "@zenoxai/ui/components/toggle-group"
export function TimeRange() {
return (
<ToggleGroup
variant="segmented"
defaultValue={["7d"]}
aria-label="Time range"
>
<ToggleGroupItem value="24h">24h</ToggleGroupItem>
<ToggleGroupItem value="7d">7 days</ToggleGroupItem>
<ToggleGroupItem value="30d">30 days</ToggleGroupItem>
</ToggleGroup>
)
}
Use a toggle group for two to five short options that change what the current view shows: a
time range, a layout, a set of severities. The value is always an array of item values,
even when only one item can be pressed.
Use something else when:
- The options are long, many, or need a description each: use a Radio Group or a Select.
- Each option switches to a different panel of content: use Tabs.
- There is a single on/off option: use a Toggle.
Set variant and size on ToggleGroup; every item inherits them.
| Variant | Use for |
|---|---|
segmented |
A segmented control: one choice from a few, inside one bordered track. One item always stays pressed. |
outline |
Filters and layout switches in toolbars, where items read as buttons. |
default |
Icon toolbars on a plain surface, with no borders. |
Examples
Multiple selection
Set multiple to let the user press any number of items. Here the group filters alerts by
severity.
<ToggleGroup
multiple
variant="outline"
size="sm"
defaultValue={["critical", "high"]}
aria-label="Severity"
>
<ToggleGroupItem value="critical">Critical</ToggleGroupItem>
<ToggleGroupItem value="high">High</ToggleGroupItem>
<ToggleGroupItem value="medium">Medium</ToggleGroupItem>
<ToggleGroupItem value="low">Low</ToggleGroupItem>
</ToggleGroup>Joined
spacing={0} removes the gap and merges the borders, so the items read as one control. Every
icon-only item needs an aria-label.
<ToggleGroup
variant="outline"
spacing={0}
defaultValue={["table"]}
aria-label="Layout"
>
<ToggleGroupItem value="table" aria-label="Table">
<TableIcon />
</ToggleGroupItem>
<ToggleGroupItem value="list" aria-label="List">
<ListIcon />
</ToggleGroupItem>
<ToggleGroupItem value="grid" aria-label="Grid">
<LayoutGridIcon />
</ToggleGroupItem>
</ToggleGroup>Sizes
sm (28px) for filter bars and card headers, default (32px) for page toolbars, lg (36px)
for prominent controls. The whole control is as tall as a Button of the
same size, segmented track included, so it lines up with the buttons and inputs beside it.
<div className="flex flex-col items-center gap-3">
<ToggleGroup
variant="segmented"
size="sm"
defaultValue={["open"]}
aria-label="Status, small"
>
<ToggleGroupItem value="open">Open</ToggleGroupItem>
<ToggleGroupItem value="closed">Closed</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup
variant="segmented"
defaultValue={["open"]}
aria-label="Status, default"
>
<ToggleGroupItem value="open">Open</ToggleGroupItem>
<ToggleGroupItem value="closed">Closed</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup
variant="segmented"
size="lg"
defaultValue={["open"]}
aria-label="Status, large"
>
<ToggleGroupItem value="open">Open</ToggleGroupItem>
<ToggleGroupItem value="closed">Closed</ToggleGroupItem>
</ToggleGroup>
</div>Controlled
A segmented group without multiple always keeps one item pressed: clicking the pressed item
does nothing. In the other variants the pressed item can be unpressed, which leaves the value
empty; when the view needs a value at all times, control the group and ignore empty changes in
onValueChange.
Showing open alerts.
function ToggleGroupControlled() {
const [status, setStatus] = React.useState(["open"])
return (
<div className="flex flex-col items-center gap-3">
<ToggleGroup
variant="segmented"
size="sm"
value={status}
onValueChange={setStatus}
aria-label="Alert status"
>
<ToggleGroupItem value="open">Open</ToggleGroupItem>
<ToggleGroupItem value="acknowledged">Acknowledged</ToggleGroupItem>
<ToggleGroupItem value="resolved">Resolved</ToggleGroupItem>
</ToggleGroup>
<p className="text-sm text-text-secondary">Showing {status[0]} alerts.</p>
</div>
)
}Accessibility
- The group renders
role="group"and every item is a<button>witharia-pressed. Name the group witharia-labelso screen readers announce what the options are for. - The group is one tab stop. The Left and Right arrow keys move between items (wrapping at the
ends unless
loopFocus={false}), Home and End jump to the first and last, and Space or Enter presses the focused item.
API reference
Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.
ToggleGroup
ToggleGroup is a set of toggles where one, or several with `multiple`, can be on: a view switch, a time window, text formatting. With the default `spacing` of 0 the items join into one bar; pass a positive `spacing` for separate items. `variant="segmented"` is a segmented control: the items sit inside one bordered track, and without `multiple` one item always stays pressed. To switch between panels of content use Tabs; for one choice in a form, RadioGroup.
| Prop | Type | Default |
|---|---|---|
defaultValue | readonly string[]The pressed state of the toggle group represented by an array of the values of all pressed toggle buttons. This is the uncontrolled counterpart of `value`. | - |
disabled | booleanWhether the toggle group should ignore user interaction. | false |
loopFocus | booleanWhether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. | true |
multiple | booleanWhen `false` only one item in the group can be pressed. If any item in the group becomes pressed, the others will become unpressed. When `true` multiple items can be pressed. | false |
onValueChange | ((groupValue: string[], eventDetails: { reason: "none"; event: Event; cancel: () => void; allowPropagation: () => void; isCanceled: boolean; isPropagationAllowed: boolean; trigger: Element; }) => void)Callback fired when the pressed states of the toggle group changes. | - |
orientation | "horizontal" | "vertical" | horizontal |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ToggleGroupState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
size | "default" | "sm" | "lg" | nullHeight: `sm` 28px, `default` 32px, `lg` 36px. | - |
spacing | numberGap between items, in 4px steps. 0 (default) joins them into one bar. Ignored by the segmented variant, which has its own track. | 0 |
value | readonly string[]The pressed state of the toggle group represented by an array of the values of all pressed toggle buttons. This is the controlled counterpart of `defaultValue`. | - |
variant | "default" | "outline" | "segmented" | null`default` is borderless; `outline` adds the border of an outline Button; `segmented` is an item of a segmented ToggleGroup. Every variant marks the pressed state the same way, with the selection colors (primary-soft fill and text) that FilterChip and the sidebar use, so pressed never looks like hover. | - |
ToggleGroupItem
ToggleGroupItem is one toggle of a ToggleGroup, identified by its `value`. A `variant` or `size` set on the group overrides its own.
| Prop | Type | Default |
|---|---|---|
defaultPressed | booleanWhether the toggle button is currently pressed. This is the uncontrolled counterpart of `pressed`. | false |
disabled | booleanWhether the component should ignore user interaction. | false |
nativeButton | booleanWhether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). | true |
onPressedChange | ((pressed: boolean, eventDetails: { reason: "none"; event: Event; cancel: () => void; allowPropagation: () => void; isCanceled: boolean; isPropagationAllowed: boolean; trigger: Element; }) => void)Callback fired when the pressed state is changed. | - |
pressed | booleanWhether the toggle button is currently pressed. This is the controlled counterpart of `defaultPressed`. | - |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ToggleState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
size | "default" | "sm" | "lg" | nullHeight: `sm` 28px, `default` 32px, `lg` 36px. | default |
value | stringA unique string that identifies the toggle when used inside a toggle group. | - |
variant | "default" | "outline" | "segmented" | null`default` is borderless; `outline` adds the border of an outline Button; `segmented` is an item of a segmented ToggleGroup. Every variant marks the pressed state the same way, with the selection colors (primary-soft fill and text) that FilterChip and the sidebar use, so pressed never looks like hover. | default |