# Toggle buttons communicate their state

- **Pattern:** `ab-000071` (`accessibility-basics.forms-interactive.toggle-state`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000071
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000071)

## Why it matters

A 'show password' toggle button that changes an input from `type="password"` to `type="text"` — but has no `aria-pressed` — is announced identically by screen readers whether the password is visible or hidden. Users cannot tell whether their action succeeded. The same problem applies to a favorites button, a bookmark toggle, a mute button, and any other binary-state control. WCAG 2.2 SC 4.1.2 (Name, Role, Value) requires that the current value or state of UI components be programmatically determinable. Section 508 2018 Refresh 502.3.4 mandates that value or state changes be programmatically determinable. The expected pattern per ARIA Authoring Practices: a toggle button uses `aria-pressed` to expose its binary state.

## Severity rationale

Medium because missing `aria-pressed` leaves screen reader users unable to determine toggle state, requiring them to infer it from side effects rather than direct feedback.

## Remediation

Add `aria-pressed` to every toggle button and update it synchronously with the state change. Combine with a descriptive `aria-label` that either reflects the current state or describes the action.

```tsx
// Show/hide password toggle
const [isVisible, setIsVisible] = useState(false);

<div className="relative">
  <input
    id="password"
    type={isVisible ? 'text' : 'password'}
    aria-describedby="password-hint"
  />
  <button
    type="button"
    aria-pressed={isVisible}
    aria-label={isVisible ? 'Hide password' : 'Show password'}
    onClick={() => setIsVisible(v => !v)}
    className="absolute right-2 top-1/2 -translate-y-1/2"
  >
    {isVisible ? <EyeOff aria-hidden="true" /> : <Eye aria-hidden="true" />}
  </button>
</div>

// Favorites button
<button
  type="button"
  aria-pressed={isFavorited}
  aria-label={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
  onClick={() => setIsFavorited(v => !v)}
>
  <HeartIcon
    aria-hidden="true"
    fill={isFavorited ? 'currentColor' : 'none'}
  />
</button>
```

Do not use `aria-checked` for buttons — that is for checkboxes and radio buttons. `aria-pressed` is the correct attribute for toggle buttons.

## Detection

- **ID:** `toggle-state`
- **Severity:** `medium`
- **What to look for:** Enumerate every relevant item. Check buttons that toggle a state (like show/hide password, theme toggle, favorites). These should use `aria-pressed` attribute to communicate their on/off state to screen readers.
- **Pass criteria:** At least 1 of the following conditions is met. Toggle buttons have `aria-pressed="true"` or `aria-pressed="false"` attribute. The attribute updates when the state changes.
- **Fail criteria:** Toggle buttons lack `aria-pressed`, or the attribute is not updated on state change.
- **Skip (N/A) when:** The application has no toggle buttons.
- **Detail on fail:** Identify toggle buttons without aria-pressed. Example: `"Show password toggle button has no aria-pressed. Theme switcher button has no state indication."`
- **Remediation:** Add aria-pressed to toggle buttons:

  ```tsx
  const ThemeToggle = ({ isDark, setIsDark }) => (
    <button
      aria-pressed={isDark}
      onClick={() => setIsDark(!isDark)}
      aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
    >
      {isDark ? '🌙' : '☀️'}
    </button>
  );
  ```

## External references

- wcag 4.1.2
- section-508 502.3.4

Taxons: accessibility

HTML version: https://auditbuffet.com/patterns/ab-000071
