ToggleButton
A button that toggles between on and off states, with the same variant, intent, size, and rounded system as Button. Shows a persistent pressed visual when active.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { ToggleButton } from "@tessinaui/ui";<ToggleButton>Star</ToggleButton>Examples
Default
A controlled toggle that swaps its label and icon as it switches on and off.
Variants
Four visual styles — primary, secondary, ghost, and outline.
Sizes
Five text sizes from xs to xl on the family scale (32 / 40 / 48 / 56 / 96 px), plus icon — a 48 px square for an icon-only toggle that requires an accessible name. xs and sm keep a 44 px minimum touch target on phones.
Intents
Semantic color intents — none, error, warning, success, and info.
States
Off, pressed, disabled, and loading states.
On color
Use tone="on-color" so toggles read correctly on a colored surface.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
pressed | boolean | — | Controlled pressed state |
defaultPressed | boolean | false | Initial pressed state for uncontrolled usage |
onPressedChange | (pressed: boolean) => void | — | Callback fired when pressed state changes |
variant | "primary" | "secondary" | "ghost" | "outline" | "primary" | Visual style |
intent | "none" | "error" | "warning" | "success" | "info" | "none" | Semantic color intent |
size | "xs" | "sm" | "md" | "lg" | "xl" | "icon" | "md" | Family scale 32 / 40 / 48 / 56 / 96 px; icon is a 48 px square and requires aria-label or aria-labelledby (enforced by the type) |
rounded | "none" | "sm" | "md" | "lg" | "xl" | "full" | "full" | Corner radius |
tone | "default" | "on-color" | inherited from <Surface> | High-contrast styling for a coloured or dark surface; pressed becomes the inverse chip |
fullWidth | boolean | false | Stretch to the container width |
showBorder | boolean | true | outline only. outline means "border at rest", as on Button; pass false for a row of toggles that should read as one quiet surface until one is on — or use variant="ghost", which is what that look is |
leadingIcon | React.ReactNode | — | Icon rendered before the label. Stays mounted while loading (the spinner overlays it) so the width never moves |
trailingIcon | React.ReactNode | — | Icon rendered after the label. Stays mounted while loading |
loading | boolean | false | Overlays a spinner, sets aria-busy + aria-disabled, swallows activation. Keeps focus and size; not natively disabled |
disabled | boolean | false | Disables the button and prevents toggling |
dir | "ltr" | "rtl" | "ltr" | Text direction — RTL reverses icon/label order via CSS flex |
Controlled vs Uncontrolled
Controlled — manage pressed state externally:
const [starred, setStarred] = useState(false);
<ToggleButton pressed={starred} onPressedChange={setStarred}>
{starred ? "Starred" : "Star"}
</ToggleButton>Uncontrolled — let the component manage its own state:
<ToggleButton defaultPressed={false} onPressedChange={(p) => console.log(p)}>
Star
</ToggleButton>Pressed intents
Accessibility
Follows the WAI-ARIA Button pattern, toggle variant.
- Renders a
<button>witharia-pressedreflecting the current toggle state data-pressedis present (empty string) when pressed and absent when not — target it as[data-pressed], not[data-pressed="true"]. A[data-pressed="false"]selector never matches anything- Keyboard accessible: Enter and Space toggle the button
- Focus ring visible with 2px ring + 2px offset (matches design system standard)
- Disabled state prevents toggling and is reflected via the native
disabledattribute loadingis not the same as disabled: the toggle keeps focus and reportsaria-busy, and both icons stay mounted so its width does not move- Under Windows high-contrast mode the pressed state maps to the system
Highlightcolours — the painted fill is stripped there, which would otherwise leave an icon-only toggle with no pressed indication at all size="icon"requiresaria-labeloraria-labelledby, enforced by the type- Touch targets ≥ 44×44px on phones and tablets (
xs/smgrow;md+ already clear it)
When to use which
- A toggle applies immediately. Bold in a toolbar, mute, a filter chip. If the change needs saving, it is a form control, not a toggle.
- For a persistent on/off setting, use Switch — a toggle button reads as "apply this now", a switch as "this is how it stays".
- For one-of-many, use ToggleGroup or SegmentedControl, which manage selection and roving focus for you.
- Keep the label identical in both states. "Star" that becomes "Starred"
changes width mid-click and makes the pressed state harder, not easier, to
read — the state is already carried by
aria-pressedand the fill.