Accessibility

Ensure your themed UI meets accessibility standards.

Accessibility

Turbo Themes targets strict WCAG 2.x AA for theme tokens and the docs site. Unit tests gate normal-text pairs at 4.5:1 (including secondary text, links, inline code, selection, blockquotes, and state/brand button labels). Headings are gated at the large-text AA floor of 3:1. End-to-end axe scans use A + AA tags with contrast, target-size, link-in-text-block, and scrollable-region-focusable enabled.

Color Contrast

WCAG Requirements

Web Content Accessibility Guidelines (WCAG) define minimum contrast ratios:

Content Type Minimum Ratio Level
Normal text 4.5:1 AA
Large text (18px+ or 14px+ bold) 3:1 AA
UI components 3:1 AA
Normal text 7:1 AAA
Large text 4.5:1 AAA

Built-in Contrast

All Turbo Themes meet WCAG AA for semantic token pairs:

/* These combinations are safe */
body {
  background: var(--turbo-bg-base);
  color: var(--turbo-text-primary); /* ✓ 4.5:1+ contrast */
}

.muted {
  color: var(--turbo-text-secondary); /* ✓ 4.5:1+ contrast */
}

.btn-primary {
  background: var(--gradient-primary, var(--turbo-brand-primary));
  color: var(--turbo-text-on-brand); /* ✓ 4.5:1+ on both gradient stops */
}

.btn-success {
  background: var(--turbo-state-success);
  color: var(--turbo-state-success-text); /* ✓ 4.5:1+ on state fill */
}

Testing Contrast

Use these tools to verify contrast:

Don’t Rely on Color Alone

Color should not be the only way to convey information:

Bad Example

<!-- Don't do this - color is the only indicator -->
<span style="color: red;">Error</span>
<span style="color: green;">Success</span>

Good Example

<!-- Better - includes icon and text -->
<span class="alert alert-danger">
  <svg aria-hidden="true"><!-- error icon --></svg>
  Error: Invalid email address
</span>

<span class="alert alert-success">
  <svg aria-hidden="true"><!-- check icon --></svg>
  Success: Changes saved
</span>

Focus Indicators

Ensure interactive elements have visible focus states:

/* Good focus styles using theme tokens */
button:focus-visible,
a:focus-visible,
input:focus-visible {
  outline: 2px solid var(--turbo-brand-primary);
  outline-offset: 2px;
}

/* Never remove focus outlines without replacement */
/* BAD: *:focus { outline: none; } */

Theme Switcher Accessibility

Keyboard Navigation

Ensure theme switchers are keyboard accessible:

<div role="listbox" aria-label="Choose theme" tabindex="0">
  <button role="option" aria-selected="true" tabindex="0">Catppuccin Mocha</button>
  <button role="option" aria-selected="false" tabindex="-1">Catppuccin Latte</button>
</div>

Screen Reader Announcements

Announce theme changes to screen readers:

function setTheme(themeName) {
  // ... change theme ...

  // Announce to screen readers
  const announcement = document.createElement('div');
  announcement.setAttribute('role', 'status');
  announcement.setAttribute('aria-live', 'polite');
  announcement.className = 'sr-only'; // Visually hidden
  announcement.textContent = `Theme changed to ${themeName}`;
  document.body.appendChild(announcement);

  setTimeout(() => announcement.remove(), 1000);
}
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

Reduced Motion

Respect users who prefer reduced motion:

/* Default: smooth transitions */
.theme-transition {
  transition:
    background-color 0.3s,
    color 0.3s;
}

/* Reduced motion: instant changes */
@media (prefers-reduced-motion: reduce) {
  .theme-transition {
    transition: none;
  }
}

High Contrast Mode

Support Windows High Contrast Mode:

@media (forced-colors: active) {
  /* Ensure borders are visible */
  .card {
    border: 1px solid CanvasText;
  }

  /* Ensure focus is visible */
  button:focus {
    outline: 2px solid Highlight;
  }
}

Light/Dark Mode Preferences

Respect system preferences as a default:

function getPreferredTheme() {
  // Check saved preference first
  const saved = localStorage.getItem('turbo-theme');
  if (saved) return saved;

  // Fall back to system preference
  if (window.matchMedia('(prefers-color-scheme: light)').matches) {
    return 'catppuccin-latte';
  }
  return 'catppuccin-mocha';
}

Component Patterns

Buttons

<button type="button" class="btn btn-primary" aria-pressed="false">
  Toggle Feature
</button>
.btn-primary {
  background: var(--turbo-brand-primary);
  color: var(--turbo-text-inverse);
  /* Ensure sufficient contrast */
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
}

.btn-primary:focus-visible {
  outline: 2px solid var(--turbo-brand-primary);
  outline-offset: 2px;
}

.btn-primary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

Form Inputs

<div class="form-group">
  <label for="email">Email address</label>
  <input
    type="email"
    id="email"
    aria-describedby="email-help email-error"
    aria-invalid="false"
  />
  <p id="email-help" class="form-help">We'll never share your email.</p>
  <p id="email-error" class="form-error" hidden>Please enter a valid email.</p>
</div>
input {
  background: var(--turbo-bg-surface);
  color: var(--turbo-text-primary);
  border: 1px solid var(--turbo-border-default);
}

input:focus {
  border-color: var(--turbo-brand-primary);
  outline: none;
  box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.25);
}

input[aria-invalid='true'] {
  border-color: var(--turbo-state-danger);
}

.form-error {
  color: var(--turbo-state-danger);
}

Alerts

<div role="alert" class="alert alert-danger">
  <svg aria-hidden="true" class="alert-icon"><!-- icon --></svg>
  <div>
    <strong>Error:</strong> Your session has expired.
    <a href="/login">Log in again</a>
  </div>
</div>

Testing Checklist

  • All text meets 4.5:1 contrast ratio
  • Large text meets 3:1 contrast ratio
  • UI components meet 3:1 contrast ratio
  • Interactive elements have visible focus states
  • Theme switcher is keyboard accessible
  • Theme changes are announced to screen readers
  • Color is not the only indicator of meaning
  • System color preferences are respected
  • Reduced motion preference is respected

Tools for Testing

  1. axe DevTools - Browser extension for accessibility testing
  2. WAVE - Web accessibility evaluation tool
  3. Lighthouse - Built into Chrome DevTools
  4. Screen readers - VoiceOver (Mac), NVDA (Windows), JAWS

Next Steps