Skip to content
UI

Toggle Group

A set of toggle buttons that share one state, for picking one option or several from a short list.

@zenoxai/ui/components/toggle-groupBase UI Markdown
<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> with aria-pressed. Name the group with aria-label so 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.

PropTypeDefault
defaultValuereadonly 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`.

-
disabledboolean

Whether the toggle group should ignore user interaction.

false
loopFocusboolean

Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys.

true
multipleboolean

When `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
renderReactElement<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" | null

Height: `sm` 28px, `default` 32px, `lg` 36px.

-
spacingnumber

Gap between items, in 4px steps. 0 (default) joins them into one bar. Ignored by the segmented variant, which has its own track.

0
valuereadonly 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.

PropTypeDefault
defaultPressedboolean

Whether the toggle button is currently pressed. This is the uncontrolled counterpart of `pressed`.

false
disabledboolean

Whether the component should ignore user interaction.

false
nativeButtonboolean

Whether 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.

-
pressedboolean

Whether the toggle button is currently pressed. This is the controlled counterpart of `defaultPressed`.

-
renderReactElement<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" | null

Height: `sm` 28px, `default` 32px, `lg` 36px.

default
valuestring

A 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