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.
FigmaDescription
A toggle button switches between two mutually exclusive states — typically active and inactive — on press. The visual treatment clearly communicates the current state so users can see what is on or off at a glance.
Filter bars, toolbar controls, and list views where users need to switch a view mode or apply a reversible filter. Common in Open Point consultation management screens and stakeholder list views.
A plain button communicates an action; a toggle button communicates a persistent state. It replaces checkbox-style controls when the interaction is immediate (no Save step) and the binary choice is best expressed as a pressable affordance rather than a form field.
Anatomy
| Part | Required? | 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
Default (outlined inactive / filled active)
The standard toggle pattern. Inactive state is outlined; active state is filled with --op-color-interactive-default.
Use for most toggle button needs — filter pills, view mode switchers, and toolbar controls.
Subtle (ghost inactive / tinted active)
Lower visual weight. Inactive has no border or fill; active uses a tinted background.
Use inside dense toolbars or when the toggle sits alongside other high-emphasis controls and visual noise needs to be reduced.
Icon-only
No visible label — icon communicates the toggle state.
Use only when the icon is universally understood (e.g. a bookmark or star) and when space is extremely constrained. Always pair with a visible tooltip and an aria-label.
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
When to use
- Switching a view between two modes, such as 'Map view' and 'List view' on a stakeholder directory.
- Toggling a filter on or off in a consultation submissions list, such as filtering by 'Unread' or 'Flagged'.
- Activating or deactivating a display option that takes effect immediately without a form submission, such as showing or hiding archived records.
- Toolbar controls in rich-text or map editors where a mode can be turned on or off.
When not to use
- When the setting persists beyond the current session and affects other users or system-level configuration — use a Switch with a Save action instead.
- When there are more than two states — use a segmented control or radio button group.
- When the action is destructive or irreversible — use a Button with a confirmation dialog.
- Inside a form that requires submission to apply changes — use a Checkbox or Switch form field instead.
- As a navigation element — use a Tab or Link.
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
| Part | Token | Value |
|---|---|---|
| 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