Toggle
A wrapper for Base UI Toggle with single/multiple selection, controlled state, vertical orientation, spacing, and variant/size controls.
Basic
Default — single selection, string options
multiple — several items active simultaneously
Options format
Icon only — use aria-label for accessibility
Icon + label
Controlled
value + onValueChange — driven by external state
italic
Orientation
orientation: vertical — stacks items, arrow keys navigate vertically
Spacing
spacing — 0 = joined (default), 1+ = gap between items
Variants
variant — default / outline
Sizes
size — sm / default / lg
Disabled
disabled — entire group non-interactive
Installation
npx shadcn@latest add @glrk-ui/toggleIf you haven't set up the prerequisites yet, check out Prerequest section.
Copy and paste the following code into your project.
'use client'
import * as React from 'react'
import { ToggleGroup as ToggleGroupPrimitive } from '@base-ui/react/toggle-group'
import { Toggle as TogglePrimitive } from '@base-ui/react/toggle'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn, getKey, getLabel, getValue, isOption } from '@/lib/utils'
const toggleVariants = cva(
"group/toggle inline-flex items-center justify-center gap-1 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: 'bg-transparent',
outline: 'border border-input bg-transparent hover:bg-muted',
},
size: {
default: 'h-9 min-w-9 px-2',
sm: 'h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-1.5 text-[0.8rem]',
lg: 'h-10 min-w-10 px-2.5',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Toggle({
className,
variant = 'default',
size = 'default',
...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & {
spacing?: number
orientation?: 'horizontal' | 'vertical'
}
>({
size: 'default',
variant: 'default',
spacing: 0,
orientation: 'horizontal',
})
type toggleGrpT = ToggleGroupPrimitive.Props &
VariantProps<typeof toggleVariants> & {
spacing?: number
orientation?: 'horizontal' | 'vertical'
}
function ToggleGroup({
className,
variant,
size,
spacing = 0,
orientation = 'horizontal',
children,
...props
}: toggleGrpT) {
return (
<ToggleGroupPrimitive
data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
data-orientation={orientation}
data-horizontal={orientation !== 'vertical' ? '' : undefined}
data-vertical={orientation === 'vertical' ? '' : undefined}
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-md data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch',
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing, orientation }}>
{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 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md 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>
)
}
type toggleItemT =
| allowedPrimitiveT
| (optionT & {
'aria-label'?: string
})
type toggleItemsT = toggleItemT[]
type toggleWrapperProps = toggleGrpT & {
options: toggleItemT[]
itemCls?: string
}
function ToggleWrapper({ options, itemCls, ...props }: toggleWrapperProps) {
return (
<ToggleGroup {...props}>
{options.map((option, i) => {
const value = getValue(option)
const label = getLabel(option)
const isObj = isOption(option)
return (
<ToggleGroupItem
key={getKey(option, i)}
value={`${value}`}
className={cn('border', itemCls, isObj && option.className)}
aria-label={isObj ? option['aria-label'] : undefined}
>
{isObj ? label : `${label}`}
</ToggleGroupItem>
)
})}
</ToggleGroup>
)
}
export {
Toggle,
toggleVariants,
ToggleGroup,
ToggleGroupItem,
ToggleWrapper,
type toggleItemT,
type toggleItemsT
}
Usage
Basic
import { ToggleWrapper } from "@/components/ui/toggle"
<ToggleWrapper options={["Bold", "Italic", "Underline"]} />Multiple selection
<ToggleWrapper options={["Bold", "Italic", "Underline"]} multiple />Options format
Options accept primitives (string | number | boolean) or objects with label, value, and optional className / aria-label:
// Primitives
<ToggleWrapper options={["Bold", "Italic", 42, true]} />
// Icon only — always provide aria-label
<ToggleWrapper
options={[
{ label: <Bold />, value: "bold", "aria-label": "Toggle bold" },
{ label: <Italic />, value: "italic", "aria-label": "Toggle italic" },
]}
/>
// Icon + label
<ToggleWrapper
options={[
{ label: <><AlignLeft /> Left</>, value: "left" },
{ label: <><AlignCenter /> Center</>, value: "center" },
]}
/>Controlled
const [value, setValue] = React.useState<string[]>(["italic"])
<ToggleWrapper
options={["Bold", "Italic", "Underline"]}
value={value}
onValueChange={(v) => setValue(v)}
multiple
/>Orientation
<ToggleWrapper options={options} orientation="vertical" />Arrow key navigation adjusts automatically — up/down for vertical, left/right for horizontal. Focus loops by default (loopFocus defaults to true).
Spacing
spacing={0} (default) joins items into a single connected group. Larger values add gap:
<ToggleWrapper options={options} spacing={0} /> {/* joined */}
<ToggleWrapper options={options} spacing={1} /> {/* 4px gap */}
<ToggleWrapper options={options} spacing={2} /> {/* 8px gap */}Variants
<ToggleWrapper options={options} variant="default" />
<ToggleWrapper options={options} variant="outline" />Sizes
<ToggleWrapper options={options} size="sm" />
<ToggleWrapper options={options} size="default" />
<ToggleWrapper options={options} size="lg" />Disabled
<ToggleWrapper options={options} disabled />Item CSS class
Apply shared CSS to all items via itemCls. Individual options override with their own className:
<ToggleWrapper
options={[
"Bold",
{ label: "Custom", value: "custom", className: "text-primary" },
]}
itemCls="font-semibold"
/>Using primitives
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle"
<ToggleGroup multiple orientation="vertical">
<ToggleGroupItem value="bold" aria-label="Toggle bold">
<Bold />
</ToggleGroupItem>
<ToggleGroupItem value="italic" aria-label="Toggle italic">
<Italic />
</ToggleGroupItem>
</ToggleGroup>Reference
toggleItemT
Prop
Type
ToggleWrapper
Prop
Type