Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/toggle-group.jsonInstalls to @/components/ui/toggle-group.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0 class-variance-authority@^0.7.1Add the components it builds on: toggle.
Copy the source into your project:
'use client'
import { Toggle as TogglePrimitive } from '@base-ui/react/toggle'
import { ToggleGroup as ToggleGroupPrimitive } from '@base-ui/react/toggle-group'
import type { VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { toggleVariants } from '@/components/ui/toggle'
import { cn } from '@/lib/utils'
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & {
spacing?: number
orientation?: 'horizontal' | 'vertical'
}
>({
size: 'default',
variant: 'default',
spacing: 2,
orientation: 'horizontal',
})
function ToggleGroup({
className,
variant,
size,
spacing = 2,
orientation = 'horizontal',
children,
...props
}: ToggleGroupPrimitive.Props &
VariantProps<typeof toggleVariants> & {
spacing?: number
orientation?: 'horizontal' | 'vertical'
}) {
const contextValue = React.useMemo(
() => ({ variant, size, spacing, orientation }),
[variant, size, spacing, orientation],
)
return (
<ToggleGroupPrimitive
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
data-orientation={orientation}
orientation={orientation}
style={{ '--gap': spacing } as React.CSSProperties}
className={cn(
'group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch',
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={contextValue}>{children}</ToggleGroupContext.Provider>
</ToggleGroupPrimitive>
)
}
function ToggleGroupItem({
className,
children,
variant = 'default',
size = 'default',
...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
return (
<TogglePrimitive
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
data-spacing={context.spacing}
className={cn(
'shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t',
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
{...props}
>
{children}
</TogglePrimitive>
)
}
export { ToggleGroup, ToggleGroupItem }A set of two-state buttons that can be toggled on or off. Built on the Base UI ToggleGroup primitive with single or multiple selection modes.
Features
- Single (default) or
multipleselection - Built on the Base UI ToggleGroup primitive for accessibility
- Shares styling with the
Togglecomponent (toggleVariants) variantandsizeset on the group are inherited by every itemspacingprop controls the gap between items;spacing={0}joins items into a segmented control- Horizontal and vertical
orientation - Icon spacing via
data-icon="inline-start"/data-icon="inline-end" - Controlled and uncontrolled modes
- Individual item or group-wide disabled states
Usage
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { AlignCenter, AlignLeft, AlignRight, Bold, Italic } from "lucide-react"
// Single selection
<ToggleGroup defaultValue={["left"]}>
<ToggleGroupItem value="left" aria-label="Align left">
<AlignLeft />
</ToggleGroupItem>
<ToggleGroupItem value="center" aria-label="Align center">
<AlignCenter />
</ToggleGroupItem>
<ToggleGroupItem value="right" aria-label="Align right">
<AlignRight />
</ToggleGroupItem>
</ToggleGroup>
// Multiple selection, segmented outline
<ToggleGroup multiple variant="outline" spacing={0}>
<ToggleGroupItem value="bold">
<Bold data-icon="inline-start" />
Bold
</ToggleGroupItem>
<ToggleGroupItem value="italic">
<Italic data-icon="inline-start" />
Italic
</ToggleGroupItem>
</ToggleGroup>
// Vertical
<ToggleGroup orientation="vertical">
...
</ToggleGroup>Examples
Default
A joined (spacing={0}) outline group filtering cases by status. Give the group an aria-label so the set has a name.
Single Selection
By default one item can be pressed. The value is always an array, so a single selection is value={[language]}. Here it
picks the language of a decision letter; ignoring the empty array in onValueChange keeps one option always selected.
<ToggleGroup value={[language]} onValueChange={(value) => value[0] && setLanguage(value[0])}>
…
</ToggleGroup>Multiple Selection
multiple lets any number of items be pressed – the weekdays for home help visits. Each short label has a full
aria-label.
Variants
default and outline, set once on the group and inherited by every item.
Sizes
sm, default and lg.
Spacing
spacing={0} joins the items into a segmented control; any other value is the gap in spacing units.
Vertical
orientation="vertical" stacks the items – here as a filter for e-service areas, with counts.
With Icon and Text
Icons with data-icon="inline-start" next to text labels, for choosing how a decision is delivered.
View Modes
Icon-only items switching between list, grid and map views of municipal facilities.
Rating
A five-point satisfaction scale with icon-only items and a live text label for the current choice.
Disabled
Disable the whole group with disabled on ToggleGroup, or single items with disabled on ToggleGroupItem.
Props
ToggleGroup
multiple?: boolean- Whether several items can be pressed at once (defaultfalse)value?: string[]- Controlled values of the pressed items (always an array, also for single selection)defaultValue?: string[]- Default values for uncontrolled modeonValueChange?: (value: string[], eventDetails) => void- Callback when selection changesloopFocus?: boolean- Whether arrow keys wrap from the last item to the first (defaulttrue)variant?: "default" | "outline"- Visual style applied to all itemssize?: "sm" | "default" | "lg"- Size applied to all itemsspacing?: number- Gap between items in spacing units (default2;0joins items)orientation?: "horizontal" | "vertical"- Layout direction (default"horizontal")disabled?: boolean- Whether the entire group is disabled- Extends all Base UI ToggleGroup props
ToggleGroupItem
value: string- Unique identifier for the item (required)variant?: "default" | "outline"- Used when the group does not set a variantsize?: "sm" | "default" | "lg"- Used when the group does not set a sizedisabled?: boolean- Whether this specific item is disabled- Extends all Base UI Toggle props
Styling
Items use toggleVariants from @/components/ui/toggle. The group exposes data-variant, data-size,
data-spacing and data-orientation attributes, and items expose data-slot="toggle-group-item", so you can target
them with Tailwind data selectors:
<ToggleGroup className="rounded-lg bg-muted p-1">
<ToggleGroupItem value="a" className="data-pressed:bg-background">A</ToggleGroupItem>
<ToggleGroupItem value="b" className="data-pressed:bg-background">B</ToggleGroupItem>
</ToggleGroup>Accessibility
- Built on the Base UI ToggleGroup primitive with full keyboard support
- Roving focus:
Tabmoves into and out of the group, arrow keys move between items Space/Entertoggle the focused itemHome/Endmove to the first / last item- Provide an
aria-labelon icon-only items, and on the group when its purpose is not obvious
Best Practices
-
Selection Modes
- Use the default single selection for mutually exclusive options (radio-like behavior)
- Use
multiplefor independent toggles that can be combined
-
Labeling
- Always provide meaningful
aria-labelattributes for icon-only items - Consider adding tooltips for icon-only items
- Always provide meaningful
-
Visual Design
- Use
spacing={0}withvariant="outline"for segmented controls - Keep the number of items manageable (typically 2-7 items)
- Use
-
State Management
- Use controlled mode when the value lives elsewhere
- Provide sensible default values
Common Use Cases
- Text Formatting: Bold, italic, underline in rich text editors
- Layout Controls: Text alignment, view modes, display options
- Filter Controls: Show/hide content categories or types
- Media Controls: Playback options, audio settings
- Segmented Controls: Day/week/month and similar period pickers
Integration Examples
With Form Libraries
import { useForm, Controller } from 'react-hook-form'
function PreferencesForm() {
const { control, handleSubmit } = useForm()
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="notifications"
control={control}
render={({ field }) => (
<ToggleGroup
multiple
value={field.value || []}
onValueChange={field.onChange}
>
<ToggleGroupItem value="email">Email</ToggleGroupItem>
<ToggleGroupItem value="push">Push</ToggleGroupItem>
<ToggleGroupItem value="sms">SMS</ToggleGroupItem>
</ToggleGroup>
)}
/>
</form>
)
}Related Components
- Toggle
- Button Group
- Radio Group
- Tabs