Skip to contentVibraUI
Inputs & filters

Toggle

A button that stays pressed: bold, pin, show grid.

Vibra transitions background, colour and shadow rather than all, and sets the sm size's label in the 13px register instead of 0.8rem; pressed is the kit's selected fill, --brand-muted, with the label in ink — not shadcn's muted plane, which measured 1.10:1 against the card and was the plane hover paints too. An unpressed toggle keeps its label in the quieter tier (muted-foreground) and the pointer gives it half the muted plane, so pressed and a hovered unpressed toggle sit 1.17:1 apart in the light and 1.21:1 in the dark (they were 1.11:1) and differ by the ink step in the label besides; a pressed toggle keeps its fill under the pointer, and a page's own hover:bg-* replaces the unpressed hover. Keyboard focus is the kit's focus-ring, the solid 2px accent outline Button and NavigationMenu wear, not shadcn's half-strength halo — and a toggle group's items, which are this toggle, carry it too.

Install

npx shadcn@latest add @vibra/toggle

Needs the @vibra registry in your components.json — set it up once.

Examples

With text

Words alone, because the words are the setting, beside the figure they change.

With icon

Icon-only toggles over a table. aria-label names each one, and a tooltip shows the same words to the pointer and the keyboard.

Pressed

A controlled toggle whose name never changes. aria-pressed carries the state, the star fills, and a status line says what it did.

Disabled, with the reason

A disabled toggle leaves the tab order, so the reason is written beside it rather than hidden in a tooltip, and describes it.

Formatting toolbar

Bold, italic, underline and strikethrough in one named multiple group. Tab enters once and the arrow keys move between them.

Filter chip

Toggles as filter chips. On, a chip takes the selected plane and a tick, and a status line gives the count and the total.

Props

PropTypeDefaultDescription
variant"default" | "outline""default"Bare, or bounded by the --input line.
size"default" | "sm" | "lg""default"32px tall by default; 28px or 36px.
pressed / defaultPressedbooleanfalseWhether it is pressed: controlled, or where it starts. It is announced through aria-pressed, so the label stays the same either way.
onPressedChange(pressed: boolean) => void—Called with the new state on every press.
disabledbooleanfalseDims it and takes it out of the tab order, so write the reason beside it and point aria-describedby at the words.

Dependencies

Source

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(
  // The kit's one focus treatment, `focus-ring` — a solid 2px outline in the
  // accent, as on Button and NavigationMenu — in place of shadcn's 3px halo at
  // half strength, which measured 1.98:1 on white. Pressed is the kit's
  // selected fill, --brand-muted, with the label in ink; unpressed keeps its
  // label in the quieter tier, and the pointer gives it half the muted plane —
  // well short of the selected fill, so a hovered toggle never looks pressed.
  // Pressed is written after hover and outranks it, so a pressed toggle under
  // the pointer still reads as pressed, and a page's own `hover:bg-*` still
  // replaces the unpressed hover.
  "group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap text-muted-foreground transition-[background-color,color,box-shadow] focus-ring hover:bg-muted/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-pressed:bg-brand-muted aria-pressed:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-transparent",
        outline: "border border-input bg-transparent",
      },
      size: {
        default:
          "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-label has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
        lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-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 }