Button (Toggle)
Two- or three-state button that toggles between pressed and not pressed using aria-pressed.
Selection
The criteria an agent checks before retrieving this component.
Use when
- Use when a control toggles a feature or action within the current context (e.g., "Mute", "Bold", "Pin", "Enable Closed Captioning").
Try a different component when
- Do not use when the control navigates to a new URL (use
link.basic). - Do not use when the control represents a persistent on/off system or application setting, such as "Enable notifications", "Dark mode" (use
switch.basic). - Do not use when the control records a value to submit with a form rather than toggling something in the current context (use
checkbox.basic). - Do not use when the control opens a menu (use
menu.basic).
Must Haves
Non-negotiable structure. Every generated instance must satisfy these rules.
- Use a native
<button>element for built-in semantics and keyboard behavior. - The button has an accessible name that describes its purpose or action.
- Default strategy: represent state by changing the accessible name to the next action (e.g., "Mute" ↔ "Unmute", "Pin" ↔ "Remove pin").
- When the button has visible text, the visible text serves as the accessible name.
- When additional context is needed beyond the visible text, add it via
aria-label,aria-labelledby, or offscreen text. The visible text appears at the start of the accessible name. - For icon-only buttons, provide an accessible name using
aria-labeloraria-labelledby. - Icons within buttons must be decorative (
aria-hidden="true"). - If the action is unavailable, disable the button using the native
disabledattribute. (It becomes unfocusable and non-interactive.) - Ensure a visible focus state (e.g., a 2px solid outline offset by 1-2px) around the button.
Formatting toolbar exception
- If the control is a formatting toggle in a toolbar (e.g., Bold/Italic/Underline), use aria-pressed="true|false" to reflect whether formatting is currently applied.
- In this toolbar case, keep the accessible name stable (e.g., "Bold") and do not rename it to "Remove bold" or "Unbold".
Donts
Avoid these accessibility and UX barriers.
- Do not use
aria-pressedfor non-toolbar toggles if you are already changing the accessible name to the next action (avoid conflicting models like "Unmute, pressed"). - Do not leave
aria-pressedincorrect, stale, or always"true"/ always"false"when you choose the toolbar approach. - Do not ship icon-only toggles without an accessible name (
aria-labeloraria-labelledby). - Do not put state only in the icon (screen reader users must get state via the accessible name change or
aria-pressed, depending on strategy).
Customizable
Alternatives and options that give the AI agent some room to move.
- For most toggles (non-toolbar), you may express "next action" via:
- Visible text (preferred when space allows), and/or
aria-label/aria-labelledby(required for icon-only).
- You may add context to the accessible name when multiple similar toggles exist (e.g., "Mute Trailer", "Unmute Trailer") using
aria-label,aria-labelledby, or offscreen text.
Golden Pattern
The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.
export function ToggleButtonDemo() {
const [muted, setMuted] = useState(false);
const [iconOnlyMuted, setIconOnlyMuted] = useState(false);
const [pinned, setPinned] = useState(false);
const [bold, setBold] = useState(false);
return (
<div>
<p>Toggle state indicated by accessible name change</p>
<button type="button" onClick={() => setMuted((v) => !v)}>
<span aria-hidden="true">[icon]</span>{" "}
{muted ? "Unmute" : "Mute"}
</button>
<button type="button" onClick={() => setPinned((v) => !v)}>
<span aria-hidden="true">[icon]</span>{" "}
{pinned ? "Unpin" : "Pin"}
</button>
<button
type="button"
onClick={() => setIconOnlyMuted((v) => !v)}
aria-label={iconOnlyMuted ? "Unmute" : "Mute"}
>
<span aria-hidden="true">[icon]</span>
</button>
<hr />
<p>Toggle state indicated by aria-pressed (toolbar formatting)</p>
<button
type="button"
aria-pressed={bold ? "true" : "false"}
onClick={() => setBold((v) => !v)}
>
<span aria-hidden="true">[icon]</span>{" "}
Bold
</button>
</div>
);
}
Acceptance Checks
The component’s test spec — an optional body of checks for verification.
Keyboard
- Tab to each control: a visible focus indicator is present.
- Press Space or Enter: the control activates/toggles.
- Either the button's accessible name adjusts to reflect its state (preferred), or it remains constant and the value of
aria-pressedreflects its state - Icons are not announced (decorative via
aria-hidden="true").