Toggle
A two-state button that is either off or pressed. Every variant is quiet when off and fills only when pressed — ghost neutral, accent the indigo accent, primary the bold primary. Its variants and xs–lg sizes mirror Button, so the two action controls feel identical under the finger.
Basic
A standalone toggle flips between off and pressed on click or with the keyboard, announcing its state through aria-pressed. Drive it with pressed + onPressedChange, or let it own its state with defaultPressed.
"use client";
import { Toggle } from "@stridge/noctis";
export default function ToggleBasic() {
return (
<div className="flex items-center gap-3">
<Toggle defaultPressed>Bold</Toggle>
<Toggle>Italic</Toggle>
<Toggle disabled>Underline</Toggle>
</div>
);
}
Variants
Four looks, named to match Button — each quiet when off, each lighting a different fill when pressed. ghost (the default, for dense toolbars) fills neutral; accent fills the indigo accent for a prominent, signalling on/off; primary fills the bold primary; outline carries a resting border so a single toggle reads as a control on bare ground, then fills primary when pressed.
"use client";
import { Toggle } from "@stridge/noctis";
const VARIANTS: Toggle.Variant[] = ["ghost", "accent", "primary", "outline"];
export default function ToggleVariants() {
return (
<div className="flex flex-col gap-4">
{VARIANTS.map((variant) => (
<div key={variant} className="flex items-center gap-3">
<Toggle variant={variant} defaultPressed>
Pressed
</Toggle>
<Toggle variant={variant}>Off</Toggle>
</div>
))}
</div>
);
}
Sizes
Four heights — xs, sm, md, and lg — sharing the control type and spacing rhythm with Button. Reach for xs in a dense toolbar; keep sm as the floor for touch targets.
"use client";
import { Toggle } from "@stridge/noctis";
const SIZES: Toggle.Size[] = ["xs", "sm", "md", "lg"];
export default function ToggleSizes() {
return (
<div className="flex items-center gap-3">
{SIZES.map((size) => (
<Toggle key={size} size={size} defaultPressed>
{size.toUpperCase()}
</Toggle>
))}
</div>
);
}
Single-select group
Without attached, ToggleGroup is a separated row sharing its variant and size. By default it is single-select — pressing one releases the others. Each toggle takes a value; the group's value is the array of pressed values.
"use client";
import { ToggleGroup, Toggle } from "@stridge/noctis";
import { AlignCenter, AlignLeft, AlignRight } from "lucide-react";
export default function ToggleGroupSingle() {
return (
<ToggleGroup aria-label="Text alignment" defaultValue={["left"]}>
<Toggle value="left" startIcon={AlignLeft} iconOnly aria-label="Align left" />
<Toggle value="center" startIcon={AlignCenter} iconOnly aria-label="Align center" />
<Toggle value="right" startIcon={AlignRight} iconOnly aria-label="Align right" />
</ToggleGroup>
);
}
Multiple-select group
Set multiple to let several toggles be pressed at once — the pattern for independent formatting controls like bold, italic, and underline.
"use client";
import { ToggleGroup, Toggle } from "@stridge/noctis";
import { Bold, Italic, Underline } from "lucide-react";
export default function ToggleGroupMultiple() {
return (
<ToggleGroup aria-label="Text formatting" multiple defaultValue={["bold"]}>
<Toggle value="bold" startIcon={Bold} iconOnly aria-label="Bold" />
<Toggle value="italic" startIcon={Italic} iconOnly aria-label="Italic" />
<Toggle value="underline" startIcon={Underline} iconOnly aria-label="Underline" />
</ToggleGroup>
);
}
Select-all (mixed)
A composite "select all" toggle that drives several children reads as mixed when only some are pressed — a half-state fill between rest and pressed. Set aria-pressed="mixed" on that toggle yourself; it is a documented pattern, not a prop. Never swap the toggle's label between states.
"use client";
import { Toggle, ToggleGroup } from "@stridge/noctis";
import { useState } from "react";
const KEYS = ["bold", "italic", "underline"];
export default function ToggleMixed() {
const [value, setValue] = useState<string[]>(["bold"]);
const allOn = value.length === KEYS.length;
// A composite "select all" reads `mixed` when only some children are pressed — the half-state fill
// sits between rest and pressed. `aria-pressed="mixed"` is set by the consumer; it is not a prop.
const masterPressed = allOn ? "true" : value.length > 0 ? "mixed" : "false";
return (
<div className="flex items-center gap-3">
<Toggle
variant="outline"
aria-pressed={masterPressed}
pressed={allOn}
onPressedChange={(next) => setValue(next ? [...KEYS] : [])}
>
All formatting
</Toggle>
<ToggleGroup aria-label="Text formatting" multiple value={value} onValueChange={(next) => setValue(next as string[])}>
<Toggle value="bold">Bold</Toggle>
<Toggle value="italic">Italic</Toggle>
<Toggle value="underline">Underline</Toggle>
</ToggleGroup>
</div>
);
}
Icon-only toggles
Square a toggle with iconOnly for a single glyph, and always give it an aria-label — there is no visible text to name it. For sighted discoverability, pair it with a Tooltip so its purpose is revealed on hover and focus. Don't mix icon-only and text toggles within one group, and don't change a toggle's icon between pressed states (that's an action button, not a toggle).
"use client";
import { Toggle, Tooltip } from "@stridge/noctis";
import { Bold, Italic, Underline } from "lucide-react";
const ITEMS = [
{ value: "bold", label: "Bold", icon: Bold },
{ value: "italic", label: "Italic", icon: Italic },
{ value: "underline", label: "Underline", icon: Underline },
];
export default function ToggleTooltip() {
return (
<div className="flex items-center gap-1">
{ITEMS.map(({ value, label, icon }) => (
<Tooltip.Root key={value}>
<Tooltip.Trigger render={<Toggle iconOnly startIcon={icon} aria-label={label} />} />
<Tooltip.Popup>{label}</Tooltip.Popup>
</Tooltip.Root>
))}
</div>
);
}
Keyboard
A standalone toggle is a button; a group adds roving arrow-key navigation between its toggles.
| Key | Action |
|---|---|
Space / Enter | Toggle the focused button's pressed state. |
Arrow Right / Arrow Left | Move focus to the next / previous toggle in a horizontal group. In RTL the directions mirror. |
Arrow Down / Arrow Up | Move focus to the next / previous toggle in a vertical group. |
Home / End | Move focus to the first / last toggle in a group. |
Tab | Move focus into and out of the group (the group is a single tab stop). |
Accessibility
- Pressed state — each toggle is a native
<button>carryingaria-pressed; Base UI keeps it in sync. Space and Enter activate it. - Group role — a
ToggleGroupbecomes arole="toolbar"once it holds three or more toggles, per the APG, and staysrole="group"below that. A toolbar requires anaria-label(oraria-labelledby) — an unlabeled one is an axe finding. You can override the computedroleby passing your own. - Orientation — set
orientation="vertical"and the arrow keys andaria-orientationfollow; in RTL the horizontal arrow directions mirror automatically. - Group disabled —
disabledon the group cascades to every toggle inside. - Icon-only — supply an
aria-label; pair with a Tooltip for sighted discoverability. Never change the label or icon on toggle. - Mixed — reserve
aria-pressed="mixed"for a composite "select all" toggle whose children don't all share a value.
Anatomy
Compose a single toggle directly, or wrap several in a group.
Toggle— one two-state button. Passpressed/defaultPressedstandalone, or avalueinside a group. Takes avariant(ghost/accent/primary/outline),size(xs–lg), optionalstartIcon/endIconglyphs, andiconOnlyto square it for a single glyph.variant,size, andiconOnlyare inherited from an enclosing group when unset.ToggleGroup— the container. Owns the pressed set (controlled viavalue/onValueChange, or uncontrolled viadefaultValue), shares itsvariant/size/iconOnlywith the toggles, and switches between single- and multiple-select withmultiple.
Each part carries a data-slot (toggle, toggle-group) for host-side styling — pair it with the data attributes (data-pressed, data-disabled, data-orientation, data-multiple). The group is fully keyboard-operable and RTL-aware.
On surfaces
The same control re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well. It stays legible on every layer.
Design tokens
Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every toggle in that region retunes — e.g. .toolbar { --noctis-toggle-border-radius: var(--noctis-radius-sm); } softens the corners. See Customization for the full override ladder and Tokens for the whole graph.
API reference
Generated from the component's types — every prop, type, default, and description comes straight from the source.