Glrk UI

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/toggle

If you haven't set up the prerequisites yet, check out Prerequest section.

Copy and paste the following code into your project.

ui/toggle.tsx
'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