Skip to content

Toggle Group

Group related options into a single or multiple-choice control.

import { ToggleGroup, ToggleGroupItem } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use a toggle group for a small set of options that change a view in place: a layout switcher, a time range, text formatting. Use Tabs when each option has its own panel of content, a Radio Group for a choice inside a form, and a Select when there are more options than fit in a row.

import { ToggleGroup, ToggleGroupItem } from '@youj/react'

export function RangePicker() {
  return (
    <ToggleGroup variant="segmented" aria-label="Range" defaultValue={['7d']}>
      <ToggleGroupItem value="24h">24h</ToggleGroupItem>
      <ToggleGroupItem value="7d">7d</ToggleGroupItem>
      <ToggleGroupItem value="30d">30d</ToggleGroupItem>
    </ToggleGroup>
  )
}

The value is always an array of the pressed items' values, in single and multiple mode alike. In single mode, pressing the pressed item releases it and the value becomes empty. A view switcher should always have a view, so control value and ignore the change that would empty it, as the first preview does:

<ToggleGroup value={view} onValueChange={(next) => next.length > 0 && setView(next)}>

Reference

Propssize, variant, spacing… (5)

Props

ToggleGroup

ToggleGroup props
PropType
size'default' | 'sm' | 'lg'

default 'default'

'default'
variantToggleGroupVariant
'default''outline''pill''segmented'

default 'default'

'default'
spacingnumber

Gap between items in spacing steps. 0 joins them.

default variant === 'segmented' || variant === 'pill' ? 0 : 1

variant === 'segmented' || variant === 'pill' ? 0 : 1

spacing is a gap in spacing steps: 0 joins the items, and the default is 1, or 0 for segmented and pill. These Base UI props control the group:

PropTypeDescription
valuestring[]Controlled pressed values.
defaultValuestring[]Initial pressed values when uncontrolled.
onValueChange(value: string[], details) => voidCalled with the new array when an item is pressed.
multiplebooleanLet several items be pressed at once. Default false.
orientation'horizontal' | 'vertical'Layout and arrow-key direction. Default 'horizontal'.
loopFocusbooleanWrap focus from the last item to the first. Default true.
disabledbooleanDisable every item.

ToggleGroupItem

ToggleGroupItem props
PropType
variant'default' | 'outline'–
size'default' | 'sm' | 'lg'–

Each item takes a value and, like a Toggle, disabled. It inherits variant and size from the group unless you set them.

MotionIndicator, indicator appears, press… · reduced motion

Motion

Motion tokens
MomentTokens
Indicatorsegmented or pill, single choice: glides to the pressed item
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Indicator appearswhen an empty group gets its first value
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)
Presseach item scales to 0.97 while held
duration.instantease.standard

80ms · cubic-bezier(0.2, 0, 0, 1)

80mscubic-bezier(0.2, 0, 0, 1)
Release and hoverscale back, background and text colors
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)

Reduced motion

The indicator jumps to the pressed item instead of gliding, and items don't scale when pressed.

The glide is transform-only layout projection, so a pill key's corners stay round in flight and nothing around it reflows.

Reduced motion

The indicator moves straight to the pressed item with no glide, and presses don't scale. Which item is pressed reads exactly the same.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • The group is a group named by its aria-label, and every item is a <button> with aria-pressed.
  • It's a single tab stop. The arrow keys move focus between items and wrap at the ends, Home and End jump to the first and last item, and Space or Enter presses the focused one.
  • The sliding indicator is decorative (aria-hidden) and isn't an item, so roving focus only visits the real toggles.
  • Name icon-only items with aria-label.