Components

Toggle

A two-state button that flips between OFF (idle) and ON (pressed) on every click. Built on Radix Toggle. Two variants (default: transparent; outline: bg-background + border-input + shadow-xs) and three sizes (sm=h-8 / default=h-9 / lg=h-10). All text-sm font-medium, rounded-md. When pressed (data-[state=on]), the surface goes bg-accent and the text/icon go text-foreground. The atom is what ToggleGroup composes.

Updated Jul 18, 2026 by Leonardo Posada

Anatomy

One button primitive with an icon + optional text label. Radix owns the toggled/pressed state and exposes it via data-[state=on]. The kit maps the Yuno spec via a CVA — variant × size, plus a shared base with the hover / pressed / focus / disabled states.

  1. 1
    Toggle (Root)

    Radix Root button. Uncontrolled by default (defaultPressed); pass pressed + onPressedChange for controlled toggles.

  2. 2
    Icon slot

    Phosphor weight=light icon size-4 at the left. Some Dashboard toggles swap to fill on the on-state to reinforce the pressed shape (e.g. Star vs Star-fill for favorite).

  3. 3
    Label

    Optional text-sm font-medium label to the right of the icon. Follows the same tone as the icon in the current state.

  4. 4
    Focus ring

    3px ring-ring/50 outline shadow — no offset, no ring color change. Sonner-style focus that never clips the icon.

Variants

Two variants and three sizes cover every Yuno case. Default is the light, chrome-less shape you drop into any toolbar or filter row. Outline is the settled, bordered shape for surfaces where the toggle needs to look like a real button (settings pane, empty state, controlled panel header).

Default (transparent chrome)
<Toggle aria-label="Bold">
  <TextB weight="light" className="size-4" />
</Toggle>
Outline (bordered + shadow-xs)
<Toggle variant="outline" aria-label="Bold">
  <TextB weight="light" className="size-4" />
</Toggle>
Sizes
<Toggle size="sm" />
<Toggle size="default" />
<Toggle size="lg" />

States

Idle (OFF): text-foreground with the variant's chrome. Hover: bg-muted; on the Default variant text becomes text-muted-foreground (Figma spec — hover softens instead of intensifying, to signal 'this is a target, not the state'); on the Outline variant text becomes text-accent-foreground. Pressed / on (data-[state=on]): bg-accent + text-foreground on both variants. Focus: 3px ring-ring/50 outline. Disabled: opacity-50 + pointer-events-none.

Toggle vs Switch vs ToggleGroup
Toggle is a stateful button — 'is this shape pressed right now?'. It's the right pick for formatting (Bold, Italic), view modes, and one-off include / exclude filters. Reach for Switch when the value is a real user preference the app persists (Enable notifications). Reach for ToggleGroup when you need 2+ toggles that share exclusivity (single) or a shared multi-select surface (multiple) — ToggleGroup owns the compound wiring so you don't have to.

Recipes

Ready-to-copy compositions covering the most common Yuno usages of this atom.

Bold formatter (icon-only)

The canonical text-editor use: an icon-only toggle for Bold. Wrap with a Tooltip for the label. TextB (light) as OFF, TextB (fill) as ON — the tone shifts feed on top of the state change. Sits in a toolbar row next to sibling toggles (italic, underline).

const [bold, setBold] = React.useState(false);

<Tooltip>
  <TooltipTrigger asChild>
    <Toggle aria-label="Bold" pressed={bold} onPressedChange={setBold}>
      <TextB weight={bold ? "fill" : "light"} className="size-4" />
    </Toggle>
  </TooltipTrigger>
  <TooltipContent>Bold</TooltipContent>
</Tooltip>
Filter chip (icon + text)

For quick include / exclude filters at the top of a Table: icon + text label. Use the Outline variant so the chip reads like a control, not a decoration. Pair with a controlled state so the parent Table can react to onPressedChange.

const [live, setLive] = React.useState(true);

<Toggle
  variant="outline"
  pressed={live}
  onPressedChange={setLive}
  aria-label="Live"
>
  <span className="text-sm">Live</span>
</Toggle>
View mode toggle (grid vs list)

A single toggle that flips between GridFour (light) and List (light) icons. Common in the Dashboard on a list surface where the user picks the density they want. If you need 3+ modes, promote to ToggleGroup type='single'.

const [grid, setGrid] = React.useState(true);

<Toggle
  aria-label={grid ? "Switch to list view" : "Switch to grid view"}
  pressed={grid}
  onPressedChange={setGrid}
>
  {grid ? <GridFour weight="light" className="size-4" /> : <List weight="light" className="size-4" />}
</Toggle>
Sidebar collapse trigger

Outline sm toggle in the top-right of a Sidebar section. CaretLeft (light) as OFF, CaretRight (light) as ON. onPressedChange lets the parent Sidebar shrink to the icon rail. Focus ring stays visible even when the sidebar is docked.

const [collapsed, setCollapsed] = React.useState(false);

<Toggle
  variant="outline"
  size="sm"
  pressed={collapsed}
  onPressedChange={setCollapsed}
  aria-label={collapsed ? "Expand" : "Collapse"}
>
  {collapsed ? <CaretRight weight="light" className="size-4" /> : <CaretLeft weight="light" className="size-4" />}
</Toggle>
Favorite (light ↔ fill swap)

Icon-only Toggle that swaps the Phosphor weight when pressed. Fill goes text-primary so the star reads as active.

const [favorite, setFavorite] = React.useState(false);

<Toggle
  aria-label={favorite ? "Remove from favorites" : "Add to favorites"}
  pressed={favorite}
  onPressedChange={setFavorite}
>
  <Star weight={favorite ? "fill" : "light"} className={favorite ? "size-4 text-primary" : "size-4"} />
</Toggle>

Import

One import from @/components/ui/toggle. toggleVariants is exposed so ToggleGroup and other compounds can share the exact same chrome.

import { Toggle, toggleVariants } from "@/components/ui/toggle";

Props

Toggle forwards every Radix Toggle prop. The Yuno-specific surface is the CVA variants (variant, size) — everything else (pressed, defaultPressed, onPressedChange, disabled, asChild) comes from Radix.

PropTypeDefaultDescription
variant"default" | "outline""default"default = transparent chrome; outline = bg-background + border-input + shadow-xs. Pick outline when the toggle needs to look like a button in its own right.
size"sm" | "default" | "lg""default"sm = h-8 px-1.5; default = h-9 px-2; lg = h-10 px-2.5. All share text-sm font-medium.
pressedbooleanControlled pressed state. Pair with onPressedChange. If you don't pass this, use defaultPressed for an uncontrolled toggle.
defaultPressedbooleanfalseUncontrolled initial pressed state. Radix owns the state after mount.
onPressedChange(pressed: boolean) => voidFires on every click / keyboard activation. The parent hook usually flips a piece of state.
disabledbooleanfalseDisables the toggle — opacity-50 + pointer-events-none. Keep the pressed state accurate even when disabled.
asChildbooleanfalseMerge props onto the child (Radix Slot). Use when you must wrap a link or a custom element inside a Toggle shape.
aria-labelstringRequired on icon-only toggles. Describes what the toggle changes ('Bold', 'Add to favorites', 'Collapse sidebar').
classNamestringMerged onto the button via cn(). Keep additions to layout tweaks (self-start, w-fit) — never to override the pressed / hover surfaces.

When to use

  • Text formatting in an editor toolbar (Bold, Italic, Underline).
  • View mode flip between two options (grid ↔ list, day ↔ week).
  • One-off include / exclude filter chips at the top of a Table.
  • Collapsible surfaces where the trigger stays visible after collapse (sidebar / filter panel).

When not to use

  • Persistent user preferences (dark mode, notifications) — use Switch, its shape signals 'this changes a setting'.
  • One-of-many exclusive picks — use ToggleGroup type='single' or RadioGroup.
  • Actions that fire without a state change (Save, Delete) — use Button.
  • Long lists of options — Select or Command scales; a row of toggles doesn't.

Usage

Do
  • Pair every icon-only Toggle with a Tooltip so keyboard users know what it toggles.
  • Swap Phosphor weights across states when it reinforces the meaning (Star light → Star fill).
  • Use controlled state (pressed + onPressedChange) whenever the toggle affects the parent.
  • Match the size to the surrounding rail — sm inside dense toolbars, default in regular chrome, lg in mobile-adjacent surfaces.
Don't
  • Don't use a Toggle for a decision that persists across sessions — that's a Switch.
  • Don't stack more than 4 Toggles side-by-side without a divider — cognitive load spikes.
  • Don't override the pressed styles (bg-accent + text-foreground) — the atom is deliberately monochrome; if you need color, pick Badge or a custom chip.
  • Don't mix Toggle and Button in the same row when both look like chips — pressed state gets ambiguous.

Related

Cross-links to atoms and patterns you may reach for next.

  • Toggle groupCompound for 2+ toggles with shared exclusivity (single) or shared multi-select (multiple).
  • SwitchFor persistent on/off preferences. Toggle is transient; Switch is a setting.
  • ButtonFor actions without state. Toggle is 'is this pressed right now?'.
  • Radio groupExclusive one-of-many pick when a picker feels more right than a toggle chip.
  • TooltipWraps icon-only Toggles so keyboard users know what they toggle.