A two-state button (aria-pressed) — icon and/or text, on or off.

Last updated

Uses JavaScript

Wires a click listener that flips aria-pressed on every self-toggling (non-manual) ToggleButton.

Without JS: The button renders at its initial pressed state (from the pressed prop) and stays there — clicking it does nothing, since only the script flips aria-pressed.

Show code
<ToggleButton iconOnly aria-label="Bold"><Bold /></ToggleButton>
<ToggleButton iconOnly pressed aria-label="Bold (pressed)"><Bold /></ToggleButton>

Add

npx @ava-ui/apogee add toggle-button

No Apogee dependencies. Ships one scoped JS island (progressive enhancement).

Usage

---
import ToggleButton from '@/components/apogee/ToggleButton/ToggleButton.astro'
---
<ToggleButton aria-label="Bold"><Bold /></ToggleButton>

API

@composable — exposed to Content Editors in AVA. Any other native <button> attribute (e.g. disabled) passes through.

Prop Type Default Description
pressed boolean false Initial (and, in manual mode, current) pressed state.
manual boolean false Don’t self-toggle on click — a parent (e.g. a rich-text toolbar) owns aria-pressed.
value string — Identity inside a group and the submitted value.
name string — With value, a hidden input submits the value while pressed, with or without JS.
color 'primary' | 'neutral' | 'critical' | 'positive' | 'warning' 'neutral' Resting hue.
variant 'solid' | 'faded' | 'outline' | 'ghost' 'ghost' Resting fill style (the Button tables).
size 'small' | 'medium' | 'large' | 'xlarge' 'medium' Height scale, same as Button.
selectedColor 'primary' | 'neutral' | 'critical' | 'positive' | 'warning' 'neutral' Pressed hue.
selectedVariant 'solid' | 'faded' 'faded' Pressed fill style.
rounded boolean false Fully round ends.
iconOnly boolean false Square button for a lone icon; give it an aria-label.
aria-label string — Label when the button is icon-only.
class string — Extra classes, merged last.

Variants

Every resting variant, unpressed and pressed:

Show code
<ToggleButton variant="ghost">ghost</ToggleButton>
<ToggleButton variant="ghost" pressed>ghost</ToggleButton>
<ToggleButton variant="outline">outline</ToggleButton>
<ToggleButton variant="outline" pressed>outline</ToggleButton>
<ToggleButton variant="faded">faded</ToggleButton>
<ToggleButton variant="faded" pressed>faded</ToggleButton>
<ToggleButton variant="solid">solid</ToggleButton>
<ToggleButton variant="solid" pressed>solid</ToggleButton>

Colours

Every resting color with variant="outline":

Show code
<ToggleButton variant="outline" color="primary">primary</ToggleButton>
<ToggleButton variant="outline" color="neutral">neutral</ToggleButton>
<ToggleButton variant="outline" color="critical">critical</ToggleButton>
<ToggleButton variant="outline" color="positive">positive</ToggleButton>
<ToggleButton variant="outline" color="warning">warning</ToggleButton>

Selected

selectedColor × selectedVariant style the pressed state; a solid primary pressed toggle matches a solid primary Button.

Show code
<ToggleButton pressed selectedColor="primary" selectedVariant="solid">Pressed</ToggleButton>
<Button color="primary">Button</Button>
Show code
<ToggleButton pressed selectedColor="primary" selectedVariant="faded">primary</ToggleButton>
<ToggleButton pressed selectedColor="neutral" selectedVariant="faded">neutral</ToggleButton>
<ToggleButton pressed selectedColor="critical" selectedVariant="faded">critical</ToggleButton>
<ToggleButton pressed selectedColor="positive" selectedVariant="faded">positive</ToggleButton>
<ToggleButton pressed selectedColor="warning" selectedVariant="faded">warning</ToggleButton>
<ToggleButton pressed selectedColor="primary" selectedVariant="solid">primary</ToggleButton>
<ToggleButton pressed selectedColor="neutral" selectedVariant="solid">neutral</ToggleButton>
<ToggleButton pressed selectedColor="critical" selectedVariant="solid">critical</ToggleButton>
<ToggleButton pressed selectedColor="positive" selectedVariant="solid">positive</ToggleButton>
<ToggleButton pressed selectedColor="warning" selectedVariant="solid">warning</ToggleButton>

Sizes

Every size, icon-only:

Show code
<ToggleButton iconOnly size="small" aria-label="Bold"><Bold /></ToggleButton>
<ToggleButton iconOnly aria-label="Bold"><Bold /></ToggleButton>
<ToggleButton iconOnly size="large" aria-label="Bold"><Bold /></ToggleButton>
<ToggleButton iconOnly size="xlarge" aria-label="Bold"><Bold /></ToggleButton>

States

Show code
<ToggleButton>Off</ToggleButton>
<ToggleButton pressed>On</ToggleButton>
<ToggleButton disabled>Disabled</ToggleButton>
<ToggleButton rounded pressed>Rounded</ToggleButton>
  • Pressed — the selectedVariant × selectedColor fill (default a faded neutral).
  • Hover — the resting variant’s hover fill; pressed buttons use the selected fill’s highlighted step.
  • Focus — visible focus-visible ring (--border-primary).
  • Disabled — dimmed, not interactive.
  • No JS — click does nothing; the button stays at its initial pressed value.

In a form

A hidden input submits value under name while pressed; nothing is submitted when unpressed, and a button with name but no value never submits.

Show code
<form>
<ToggleButton name="bold" value="1" pressed>Bold</ToggleButton>
<ToggleButton name="italic" value="1">Italic</ToggleButton>
<ToggleButton name="draft">No value</ToggleButton>
</form>

Examples

Text-only and icon-plus-text:

Show code
<ToggleButton>Bold</ToggleButton>
<ToggleButton><Bold /> Bold</ToggleButton>

manual for a toolbar that reads external state (e.g. the current text selection) instead of toggling itself:

<ToggleButton manual iconOnly pressed={isBoldAtSelection} aria-label="Bold">
  <Bold />
</ToggleButton>

Inside a ToggleButtonGroup the group handles single-select exclusivity and arrow-key focus.

Events

Each button dispatches apogee:toggle-change (detail: { pressed, value }, bubbling) after it toggles.

Accessibility

Key Action
Tab Moves focus to the button.
Enter Space Toggle the pressed state.
  • Real <button type="button"> with aria-pressed — the native two-state pattern, full keyboard support for free.
  • Focus — a visible focus-visible ring.
  • Icon-only usage (no visible text) always needs aria-label.
  • No JS — the button is present and focusable but inert; aria-pressed never changes. Don’t ship an icon-only, JS-dependent toggle as the only way to reach a piece of functionality.

Tokens

Token Used for
--foreground-neutral-faded / --foreground-neutral Unpressed vs. pressed label color
--background-neutral-faded-highlighted Hover fill
--background-neutral-faded Pressed fill
--outline-color-focus Focus ring

Do / Don’t

  • Do pass aria-label on every icon-only ToggleButton.
  • Do use manual when something other than a click drives the state (selection, external data) — don’t fight the self-toggle script.
  • Don’t use ToggleButton for navigation or single-fire actions — that’s Button. A toggle must represent a persistent on/off state.
  • Don’t rely on color alone to show pressed — the fill + text-color pair already carries it; don’t strip that via class.

Content

  • Icon-only: no visible label, aria-label names the action, not the state (“Bold”, not “Toggle bold”).
  • With text: a short noun or verb naming the setting (“Bold”, “Show grid”), not its state (“On”/“Off”) — the pressed style already shows that.