Skip to main content

Toggle Button

A button that switches between two states such as active and inactive. Use for immediate, reversible binary actions that do not require form submission. Don't use when the toggle controls a persistent system setting.

OverviewStyleAccessibility

Description

Anatomy

PartRequired?Notes
Container Required The pressable surface. Carries background, border, and border-radius. Minimum 44x44px touch target.
Label Required Short text describing the state or action. Should read as a noun or adjective, not a verb — for example 'Active', not 'Activate'.
Leading icon Optional Reinforces meaning visually. Use a wa-icon element. Particularly useful when the label alone may not be scannable at small sizes.
Focus ring Required 3px solid ring using --op-color-interactive-focus. Always visible on keyboard focus; never suppressed.
State indicator (pressed fill) Required The background and border change between inactive and active states. Must meet 3:1 non-text contrast ratio for the state boundary.

Variants

States

State Behaviour
Inactive (default) Button is rendered in its rest appearance — outlined border, no fill, label in --op-color-text-primary. Pressing transitions to active.
Active (pressed) Background fills with --op-color-interactive-default. Label and icon use --op-color-text-on-interactive. aria-pressed is set to 'true'.
Hover (inactive) Background tints with --op-color-interactive-hover at low opacity. Cursor is pointer.
Hover (active) Background shifts to --op-color-interactive-hover. Indicates the press will deactivate.
Focus 3px focus ring appears using --op-color-interactive-focus. Ring is offset 2px from the container edge. Visible in both inactive and active states.
Disabled Opacity 0.4. Pointer-events none. aria-disabled='true'. Do not use disabled as a permanent state — if the action is never available, remove the control.
Loading Rare. If the toggle triggers an async action, replace the label with a wa-spinner and set aria-busy='true' until resolved. Prevent re-press during loading.

Usage guidelines

Do / Don't

Do

Label the toggle with its state noun, for example 'Active' or 'Pinned', so screen readers and sighted users can tell the current state.

Don't

Label with a verb like 'Activate' — this describes an action, not a state, and becomes misleading once the button is pressed.

Do

Group related toggle buttons using role='group' with an aria-label describing the group.

Don't

Scatter independent toggle buttons without grouping context — users relying on assistive technology lose the relationship between controls.

Do

Use the icon-only variant only with a wa-tooltip and aria-label.

Don't

Rely solely on colour or icon to communicate state — always include a text label or accessible name.

Do

Keep labels short — one or two words. If the label needs more than three words, reconsider the control type.

Don't

Write sentence-length labels. Toggle buttons are not for complex choices.

Layout & Spacing

Padding: --op-space-8 (8px) vertical, --op-space-12 (12px) horizontal (default variant). Icon-only variant: --op-space-8 (8px) all sides. Gap between leading icon and label: --op-space-4 (4px). Border radius: --op-radius-md (8px) for default and subtle variants; --op-radius-pill (999px) for pill-shaped filter variants. Minimum height and width: 44px to meet touch target requirement. Focus ring offset: 2px outside the container border.

Tokens

PartTokenValue
Container (inactive) --op-color-bg-primary Background. Border: 1px solid --op-color-border-default.
Container (active) --op-color-interactive-default Background fill. Border: 1px solid --op-color-interactive-default.
Container (hover, inactive) --op-color-interactive-hover Applied at 10% opacity over --op-color-bg-primary.
Label (inactive) --op-color-text-primary Text colour in inactive state.
Label (active) --op-color-text-on-interactive Text colour on filled active background.
Leading icon (inactive) --op-color-text-secondary Icon colour in inactive state.
Leading icon (active) --op-color-text-on-interactive Icon colour in active state.
Focus ring --op-color-interactive-focus 3px solid, 2px offset. Required in all states.
Typography --op-text-sm Font size for label. Font family --op-font-body.
Disabled opacity opacity: 0.4 Applied to the whole container. No token — raw CSS.

Engineering notes

  • Use a <button> element with aria-pressed reflecting the current boolean state. Do not use <a> or <div>.
  • Toggle aria-pressed between 'true' and 'false' on click. Never remove the attribute — its presence communicates that this is a stateful toggle to assistive technology.
  • wa-button does not natively support aria-pressed. Wrap in a <button> or use a custom element that exposes the attribute. Do not use wa-button's built-in pressed appearance without also managing aria-pressed.
  • For grouped toggle buttons (e.g. filter pills), wrap in a <div role='group' aria-label='Filter by status'> to communicate the relationship.
  • Respect prefers-reduced-motion — the active/inactive transition should use a CSS transition gated on a media query. Suggested transition: background-color 150ms ease, color 150ms ease.
  • The component must be keyboard operable. Both Enter and Space should trigger the toggle.
  • If the toggle triggers an async operation, set aria-busy='true' on the button and restore it when resolved. Prevent double-submission with a loading state.

Keyboard interaction

KeyAction
Tab Moves focus to the toggle button. Focus ring becomes visible.
Shift+Tab Moves focus to the previous focusable element.
Enter Activates or deactivates the toggle. aria-pressed updates to reflect the new state.
Space Activates or deactivates the toggle. aria-pressed updates to reflect the new state.

Why it matters

Government digital services must meet WCAG 2.1 AA. Toggle buttons that rely only on colour or visual fill to communicate state will fail for users with low vision or colour blindness — and may fail an accessibility audit on a regulated government platform. The aria-pressed attribute is the mechanism that makes state legible to screen readers.

Focus

Focus is managed by the browser's natural tab order. Do not use tabindex values other than 0 or -1. In a group of toggle buttons, each button is individually focusable. Do not implement roving tabindex unless the group is explicitly a toolbar (role='toolbar'). Focus ring must remain visible at all zoom levels up to 400%.

ARIA

Role or attributeWhen to useExample
aria-pressed Set on the <button> element. Reflects the current toggle state. <button aria-pressed="true">Active</button>
aria-label Required on icon-only variants where the visible label is absent. <button aria-pressed="false" aria-label="Bookmark this stakeholder"><wa-icon name="bookmark"></wa-icon></button>
aria-disabled Use instead of the HTML disabled attribute when the button must remain focusable (e.g. to show a tooltip explaining why it is disabled). <button aria-pressed="false" aria-disabled="true">Archived</button>
role="group" + aria-label Wrap a set of related toggle buttons in a group element so screen readers announce the group context. <div role="group" aria-label="Filter submissions"><button aria-pressed="true">Unread</button><button aria-pressed="false">Flagged</button></div>
aria-busy Set to 'true' on the button while an async operation triggered by the toggle is in progress. <button aria-pressed="true" aria-busy="true">Saving...</button>

Contrast

Inactive state: label text (--op-color-text-primary) on background (--op-color-bg-primary) must meet 4.5:1. Active state: label text (--op-color-text-on-interactive) on background (--op-color-interactive-default) must meet 4.5:1. Verify this ratio when theming — green-400 is the default but custom brand colours may not meet contrast. The state boundary (the border or fill change between inactive and active) must meet 3:1 against adjacent colours for non-text contrast (WCAG 1.4.11). Disabled state at 0.4 opacity will not meet contrast ratios — this is permitted under WCAG 1.4.3 exception for disabled controls, but minimise use of disabled state.

Touch targets

Minimum 44x44px touch target is required. If the visual button is smaller (e.g. a compact icon-only variant), use padding or a transparent pseudo-element to extend the interactive area without affecting layout.

Things to avoid

  • Do not remove aria-pressed when the button is in an inactive state. Its presence on all toggle buttons — regardless of state — is what signals the control is stateful.
  • Do not use colour alone to communicate active vs inactive state. Ensure there is also a shape, fill, or typographic change.
  • Do not use role='button' on a <div> or <span> if you can use a native <button> element — native buttons handle focus, keyboard activation, and form association correctly.
  • Do not auto-focus a toggle button on page load unless it is the primary action on the screen.

Was this page helpful?

Updated 5 October 2026