Toggle Group
A set of two-state buttons that can be toggled on or off, with support for single or multiple selection.
Overview
The ToggleGroup component manages a collection of toggle buttons that can work independently or together. Built on Base UI primitives, it supports multiple selection modes, variants (including an animated segmented control style), and customizable sizes.
Usage
import { ToggleGroup, ToggleGroupItem } from "@tilt-legal/cubitt-components/primitives";<ToggleGroup multiple={true} defaultValue={["bold"]}>
<ToggleGroupItem value="bold" aria-label="Toggle bold">
<TextBold />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="Toggle italic">
<TextItalic />
</ToggleGroupItem>
</ToggleGroup>Using Cubitt's URL-state hooks, you can sync the toggle group selection with the URL by providing a paramName. This is useful for shareable links, back/forward navigation, and maintaining state across page refreshes.
// Single select - synced with ?view=week
<ToggleGroup
groupVariant="segmented"
paramName="view"
defaultValue="week"
paramClearOnDefault={true}
>
<ToggleGroupItem value="day">Day</ToggleGroupItem>
<ToggleGroupItem value="week">Week</ToggleGroupItem>
<ToggleGroupItem value="month">Month</ToggleGroupItem>
</ToggleGroup>
// Multi-select - synced with ?formats=bold,italic
<ToggleGroup
multiple
paramName="formats"
defaultValue={[]}
paramClearOnDefault={true}
>
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
</ToggleGroup>
// Advanced options:
<ToggleGroup
paramName="view"
paramClearOnDefault={true} // Remove param when value equals default
paramDebounce={300} // Debounce URL updates
onUrlValueChange={(value) => console.log("Selected:", value)}
>
{/* ... */}
</ToggleGroup>ToggleGroupItem forwards Base UI's polymorphic render support. When an item renders as something other than a native <button> such as a router link, pass nativeButton={false} so keyboard and pressed-state behavior stay correct.
import { Link } from "@tanstack/react-router";
import {
ToggleGroup,
ToggleGroupItem,
} from "@tilt-legal/cubitt-components/primitives";
<ToggleGroup defaultValue="week" groupVariant="segmented">
<ToggleGroupItem value="day">Day</ToggleGroupItem>
<ToggleGroupItem
value="week"
nativeButton={false}
render={<Link href="/week" />}
>
Week
</ToggleGroupItem>
</ToggleGroup>Examples
Segmented Variant
Toggle group with animated sliding indicator for single selection.
import { ToggleGroup, ToggleGroupItem } from "@tilt-legal/cubitt-components/primitives";
import { ToggleGroupItem } from "@tilt-legal/cubitt-components/primitives";
import {
TextAlignLeft,
TextAlignCenter,
TextAlignRight,
} from "@tilt-legal/cubitt-icons/ui/outline";
export default function Example() {
return (
<ToggleGroup groupVariant="segmented" multiple={false} defaultValue="center">
<ToggleGroupItem mode="icon" value="left" aria-label="Align left">
<TextAlignLeft />
</ToggleGroupItem>
<ToggleGroupItem mode="icon" value="center" aria-label="Align center">
<TextAlignCenter />
</ToggleGroupItem>
<ToggleGroupItem mode="icon" value="right" aria-label="Align right">
<TextAlignRight />
</ToggleGroupItem>
</ToggleGroup>
);
}Segmented Sizes
Segmented toggle groups in different sizes with animated indicator.
import { ToggleGroup } from "@tilt-legal/cubitt-components/primitives";
import { ToggleGroupItem } from "@tilt-legal/cubitt-components/primitives";
import {
TextAlignLeft,
TextAlignCenter,
TextAlignRight } from "@tilt-legal/cubitt-icons/ui/outline";
export default function Example() {
return (
<div className="flex flex-col items-start gap-6">
<div className="space-y-2">
<p className="text-sm font-medium">Small</p>
<div className="flex items-center gap-4">
<ToggleGroup groupVariant="segmented" size="sm" defaultValue="week">
<ToggleGroupItem value="day">Day</ToggleGroupItem>
<ToggleGroupItem value="week">Week</ToggleGroupItem>
<ToggleGroupItem value="month">Month</ToggleGroupItem>
<ToggleGroupItem value="year">Year</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup groupVariant="segmented" size="sm" defaultValue="center">
<ToggleGroupItem mode="icon" value="left" aria-label="Align left">
<TextAlignLeft />
</ToggleGroupItem>
<ToggleGroupItem mode="icon" value="center" aria-label="Align center">
<TextAlignCenter />
</ToggleGroupItem>
<ToggleGroupItem mode="icon" value="right" aria-label="Align right">
<TextAlignRight />
</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Medium (Default)</p>
<div className="flex items-center gap-4">
<ToggleGroup groupVariant="segmented" size="md" defaultValue="week">
<ToggleGroupItem value="day">Day</ToggleGroupItem>
<ToggleGroupItem value="week">Week</ToggleGroupItem>
<ToggleGroupItem value="month">Month</ToggleGroupItem>
<ToggleGroupItem value="year">Year</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup groupVariant="segmented" size="md" defaultValue="center">
<ToggleGroupItem mode="icon" value="left" aria-label="Align left">
<TextAlignLeft />
</ToggleGroupItem>
<ToggleGroupItem mode="icon" value="center" aria-label="Align center">
<TextAlignCenter />
</ToggleGroupItem>
<ToggleGroupItem mode="icon" value="right" aria-label="Align right">
<TextAlignRight />
</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Large</p>
<div className="flex items-center gap-4">
<ToggleGroup groupVariant="segmented" size="lg" defaultValue="week">
<ToggleGroupItem value="day">Day</ToggleGroupItem>
<ToggleGroupItem value="week">Week</ToggleGroupItem>
<ToggleGroupItem value="month">Month</ToggleGroupItem>
<ToggleGroupItem value="year">Year</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup groupVariant="segmented" size="lg" defaultValue="center">
<ToggleGroupItem mode="icon" value="left" aria-label="Align left">
<TextAlignLeft />
</ToggleGroupItem>
<ToggleGroupItem mode="icon" value="center" aria-label="Align center">
<TextAlignCenter />
</ToggleGroupItem>
<ToggleGroupItem mode="icon" value="right" aria-label="Align right">
<TextAlignRight />
</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
</div>
);
}Segmented With Separator
Use ToggleGroupSeparator inside the segmented track to split related options. The separator fades out whenever the active pill touches it, then becomes visible again once selection moves away.
import {
ToggleGroup,
ToggleGroupItem,
ToggleGroupSeparator,
} from "@tilt-legal/cubitt-components/primitives";
export default function Example() {
return (
<ToggleGroup groupVariant="segmented" defaultValue="day">
<ToggleGroupItem value="day">Day</ToggleGroupItem>
<ToggleGroupItem value="week">Week</ToggleGroupItem>
<ToggleGroupSeparator />
<ToggleGroupItem value="month">Month</ToggleGroupItem>
<ToggleGroupItem value="year">Year</ToggleGroupItem>
</ToggleGroup>
);
}Outline Variant
Toggle group with outlined buttons.
import { ToggleGroup } from "@tilt-legal/cubitt-components/primitives";
import { ToggleGroupItem } from "@tilt-legal/cubitt-components/primitives";
import {
TextBold,
TextItalic,
TextUnderline } from "@tilt-legal/cubitt-icons/ui/outline";
export default function Example() {
return (
<ToggleGroup groupVariant="outline" multiple={true} defaultValue={["bold",
"italic"]}>
<ToggleGroupItem value="bold" aria-label="Toggle bold">
<TextBold />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="Toggle italic">
<TextItalic />
</ToggleGroupItem>
<ToggleGroupItem value="underline" aria-label="Toggle underline">
<TextUnderline />
</ToggleGroupItem>
</ToggleGroup>
);
}Outline Sizes
Toggle groups in different sizes.
import { ToggleGroup } from "@tilt-legal/cubitt-components/primitives";
import { ToggleGroupItem } from "@tilt-legal/cubitt-components/primitives";
import {
TextBold,
TextItalic,
TextUnderline } from "@tilt-legal/cubitt-icons/ui/outline";
export default function Example() {
return (
<div className="flex flex-col items-start gap-6">
<div className="space-y-2">
<p className="text-sm font-medium">Small</p>
<ToggleGroup groupVariant="outline" size="sm" multiple={true} defaultValue={["bold"]}>
<ToggleGroupItem value="bold" aria-label="Toggle bold">
<TextBold />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="Toggle italic">
<TextItalic />
</ToggleGroupItem>
<ToggleGroupItem value="underline" aria-label="Toggle underline">
<TextUnderline />
</ToggleGroupItem>
</ToggleGroup>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Medium (Default)</p>
<ToggleGroup groupVariant="outline" size="md" multiple={true} defaultValue={["bold"]}>
<ToggleGroupItem value="bold" aria-label="Toggle bold">
<TextBold />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="Toggle italic">
<TextItalic />
</ToggleGroupItem>
<ToggleGroupItem value="underline" aria-label="Toggle underline">
<TextUnderline />
</ToggleGroupItem>
</ToggleGroup>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Large</p>
<ToggleGroup groupVariant="outline" size="lg" multiple={true} defaultValue={["bold"]}>
<ToggleGroupItem value="bold" aria-label="Toggle bold">
<TextBold />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="Toggle italic">
<TextItalic />
</ToggleGroupItem>
<ToggleGroupItem value="underline" aria-label="Toggle underline">
<TextUnderline />
</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
);
}URL State
Sync toggle group selection with the URL. Try selecting a value and refreshing the page.
import { ToggleGroup } from "@tilt-legal/cubitt-components/primitives";
export default function Example() {
return (
<div className="space-y-4">
<ToggleGroup
groupVariant="segmented"
paramName="view"
defaultValue="week"
paramClearOnDefault={true}
>
<ToggleGroupItem value="day">Day</ToggleGroupItem>
<ToggleGroupItem value="week">Week</ToggleGroupItem>
<ToggleGroupItem value="month">Month</ToggleGroupItem>
<ToggleGroupItem value="year">Year</ToggleGroupItem>
</ToggleGroup>
<p className="text-sm text-muted-foreground">
Check the URL - the selected value is synced with the <code>?view=</code>{" "}
parameter.
</p>
</div>
);
}Polymorphic Rendering
Render segmented toggle items through a custom link component while preserving pressed state.
import { Link } from "@tanstack/react-router";
import {
ToggleGroup,
ToggleGroupItem,
} from "@tilt-legal/cubitt-components/primitives";
export default function Example() {
return (
<ToggleGroup defaultValue="week" groupVariant="segmented" size="lg">
<ToggleGroupItem value="day">Day</ToggleGroupItem>
<ToggleGroupItem
value="week"
nativeButton={false}
render={<Link href="/week" />}
>
Week
</ToggleGroupItem>
<ToggleGroupItem
value="month"
nativeButton={false}
render={<Link href="/month" />}
>
Month
</ToggleGroupItem>
<ToggleGroupItem value="year">Year</ToggleGroupItem>
</ToggleGroup>
);
}API Reference
ToggleGroup
The root container component that manages the toggle button group.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | string[] | — | The controlled value(s) of the pressed item(s). String for single, array for multiple. |
defaultValue | string | string[] | — | The default value(s) when uncontrolled. |
onValueChange | (value: string | string[]) => void | — | Callback fired when the value changes. Returns string for single, array for multiple. |
multiple | boolean | — | When true, multiple items can be pressed. When false, only one. Auto-detected from value type if not specified. |
groupVariant | "default" | "outline" | "segmented" | "default" | Visual style variant. Segmented shows animated indicator for single selection. |
variant | "default" | "outline" | "default" | Toggle button variant (passed to items). |
size | "sm" | "md" | "lg" | "md" | Size of the toggle buttons. |
disabled | boolean | false | Whether all toggle buttons are disabled. |
loop | boolean | true | Whether keyboard focus loops back to the first item. |
orientation | "horizontal" | "vertical" | "horizontal" | Layout orientation of the toggle group. |
className | string | — | Additional CSS classes for the toggle group container. |
URL State Props
When provided with a paramName, the toggle group will sync its selection with URL parameters via TanStack Router search params.
| Prop | Type | Default | Description |
|---|---|---|---|
paramName | string | — | URL parameter name for syncing state with URL. Enables URL state management. |
onUrlValueChange | (value: string | string[] | null) => void | — | Callback when URL parameter value changes. |
paramClearOnDefault | boolean | true | Remove URL param when value equals default. |
paramThrottle | number | — | Throttle URL updates in milliseconds. |
paramDebounce | number | — | Debounce URL updates in milliseconds. |
ToggleGroupItem
Individual toggle button within a ToggleGroup.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique value for this toggle button (required). |
mode | "icon" | — | Square sizing for icon-only content. Renders as a circle in segmented variant. |
disabled | boolean | false | Whether this specific toggle button is disabled. |
variant | "default" | "outline" | — | Override the group's variant for this item. |
size | "sm" | "md" | "lg" | — | Override the group's size for this item. |
render | ReactElement | function | — | Render the item as a custom element or component while preserving toggle state. |
nativeButton | boolean | true | Set to false when render outputs a non-button element like Link or a. |
className | string | — | Additional CSS classes for the toggle button. |
children | React.ReactNode | — | Content of the toggle button (icons, text, etc.). |
Notes
- Selection modes: Set
multiple={true}for multi-select ormultiple={false}for single-select. If omitted, auto-detects from value type (array = multiple, string = single) - Multiple selection (
multiple={true}): Users can toggle multiple items on/off independently - Single selection (
multiple={false}): Only one item can be selected at a time - The
segmentedvariant with single selection (multiple={false}) provides an animated sliding indicator onValueChangereturns the appropriate format: string for single selection, array for multiple selection- For icon-only buttons, always provide
aria-labelfor accessibility - The component uses Motion for animations in segmented variant, respecting
prefers-reduced-motion