rapidUI

Toggle

Two-state button built on Base UI Toggle.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/toggle.json

Installs to @/components/ui/toggle.

Manual installation

Install the dependencies:

pnpm add @base-ui/react@^1.8.0 class-variance-authority@^0.7.1

Copy the source into your project:

components/ui/toggle.tsx
'use client'

import { Toggle as TogglePrimitive } from '@base-ui/react/toggle'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

const toggleVariants = cva(
	"group/toggle inline-flex items-center justify-center gap-1 rounded-lg 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 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-accent aria-pressed:text-accent-foreground data-pressed:bg-accent data-pressed:text-accent-foreground 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-accent hover:text-accent-foreground',
			},
			size: {
				default: 'h-10 min-w-10 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5',
				sm: "h-8 min-w-8 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
				lg: 'h-12 min-w-12 px-4 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
			},
		},
		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} />
}

export { Toggle, toggleVariants }

A two-state button that can be either on or off. Built on the Base UI Toggle primitive with customizable variants and sizes.

Features

  • Two-state toggle functionality (pressed/unpressed)
  • Built on the Base UI Toggle primitive for accessibility
  • Two visual variants: default and outline
  • Three sizes: sm, default, lg
  • Icon spacing via data-icon="inline-start" / data-icon="inline-end"; SVGs are sized automatically
  • Controlled (pressed) and uncontrolled (defaultPressed) modes
  • Disabled state and aria-invalid styling
  • toggleVariants exported for reuse (also used by ToggleGroup)

Usage

import { Toggle } from "@/components/ui/toggle"
import { Bold } from "lucide-react"

// Icon only
<Toggle aria-label="Toggle bold">
  <Bold />
</Toggle>

// With text
<Toggle aria-label="Toggle bold">Bold</Toggle>

// With icon and text
<Toggle aria-label="Toggle bold">
  <Bold data-icon="inline-start" />
  Bold
</Toggle>

// Initially pressed (uncontrolled)
<Toggle defaultPressed aria-label="Toggle bold">
  <Bold />
</Toggle>

// Controlled toggle
const [isPressed, setIsPressed] = useState(false)

<Toggle pressed={isPressed} onPressedChange={setIsPressed} aria-label="Toggle bold">
  <Bold />
</Toggle>

// Variant and size
<Toggle variant="outline" size="lg" aria-label="Toggle bold">
  <Bold />
</Toggle>

Examples

Default

An outline toggle with an icon and text. The pin icon is filled while the toggle is on, using the group/toggle state.

Variants

default is transparent until pressed; outline adds a border so the toggle reads as a control on busy backgrounds.

Sizes

Three sizes: sm, default and lg.

With Text

Small outline toggles with icons and counts, used as map layer filters for municipal facilities.

Formatting Toolbar

Independent toggles in an editor toolbar for a decision letter. The preview text follows the pressed state.

Follow Case

A toggle per row to follow public cases. The icon and label change with the state; aria-pressed tells screen readers whether it's on.

Disabled

Disabled toggles in both states and variants.

Controlled

A controlled toggle (pressed / onPressedChange) that filters a case list to overdue cases.

Props

Toggle

  • variant?: "default" | "outline" - The visual style variant (default "default")
  • size?: "sm" | "default" | "lg" - The size of the toggle (default "default")
  • pressed?: boolean - The pressed state of the toggle (controlled)
  • defaultPressed?: boolean - The default pressed state (uncontrolled)
  • onPressedChange?: (pressed: boolean, eventDetails) => void - Callback when pressed state changes
  • disabled?: boolean - Whether the toggle is disabled
  • className?: string - Additional CSS classes
  • Extends all Base UI Toggle props

Styling

The pressed state is styled with data-pressed / aria-pressed (accent background). Override it with className:

<Toggle className="data-pressed:bg-primary data-pressed:text-primary-foreground" aria-label="Toggle bold">
  <Bold />
</Toggle>

Accessibility

  • Built on the Base UI Toggle primitive with full keyboard support
  • Renders a button with aria-pressed reflecting the state
  • Space / Enter toggle the pressed state
  • Always provide an aria-label for icon-only toggles

Best Practices

  1. Labeling

    • Always provide meaningful aria-label attributes
    • Consider tooltips for icon-only toggles
    • Ensure labels describe the action, not the current state
  2. State Management

    • Use controlled mode when the state lives elsewhere
    • Use ToggleGroup for sets of related options
  3. Visual Design

    • Use consistent sizing within a toolbar
    • Use outline when toggles sit on a busy background

Common Use Cases

  • Text Formatting: Bold, italic, underline toggles in editors
  • Layout Controls: Text alignment, view modes, display options
  • Filter Controls: Show/hide content categories
  • Media Controls: Mute/unmute, shuffle/repeat

Integration Examples

With React Hook Form

import { useForm, Controller } from 'react-hook-form'
import { Toggle } from '@/components/ui/toggle'

function SettingsForm() {
  const { control, handleSubmit } = useForm()

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="notifications"
        control={control}
        render={({ field }) => (
          <Toggle
            pressed={field.value}
            onPressedChange={field.onChange}
            aria-label="Enable notifications"
          >
            Enable Notifications
          </Toggle>
        )}
      />
    </form>
  )
}
  • Toggle Group
  • Button
  • Checkbox
  • Switch
  • Radio Group
  • Tabs