crefft Source 2026.10.4

Accessibility

crefft > Design > Accessibility

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.


Overview

Accessibility ensures interfaces work for everyone, regardless of ability. This is not optional—it is a legal requirement in many jurisdictions and a moral imperative everywhere.

All projects MUST meet WCAG 2.1 Level AA compliance at minimum.

Quick Reference

CategoryKey Requirements
PerceivableText alternatives, color contrast, resizable text
OperableKeyboard accessible, no time limits, no seizure triggers
UnderstandableReadable, predictable, error prevention
RobustCompatible with assistive technologies

WCAG 2.1 AA Requirements

1. Perceivable

1.1 Text Alternatives

All non-text content MUST have text alternatives:

<!-- Images with meaning -->
<img src="chart.png" alt="Sales increased 25% from Q1 to Q2" />

<!-- Decorative images -->
<img src="decorative-line.svg" alt="" role="presentation" />

<!-- Icons with meaning -->
<svg role="img" aria-label="Warning">
  <use href="#icon-warning"></use>
</svg>

<!-- Icons that are decorative -->
<svg aria-hidden="true">
  <use href="#icon-decorative"></use>
</svg>

1.3 Adaptable Structure

Content structure MUST be programmatically determinable:

<!-- Use semantic HTML -->
<main>
  <article>
    <header>
      <h1>Page Title</h1>
    </header>
    <section aria-labelledby="section-1">
      <h2 id="section-1">Section Title</h2>
      <!-- Content -->
    </section>
  </article>
</main>

<!-- Use landmarks -->
<header role="banner">...</header>
<nav role="navigation" aria-label="Main">...</nav>
<main role="main">...</main>
<aside role="complementary">...</aside>
<footer role="contentinfo">...</footer>

1.4 Distinguishable

Color Contrast Requirements:

The table distinguishes WCAG 2.2 AA minimums from crefft’s stricter requirements. WCAG SC 1.4.3 defines large-scale text in points, not CSS pixels. The CSS pixel figures below are approximate conversions at 1pt = 1.333px.

Content TypeWCAG 2.2 AAcrefft
Normal text: below 18pt regular, or below 14pt bold4.5:14.5:1
Large text: 18pt or larger regular (approximately 24 CSS px)3:14.5:1
Large text: 14pt or larger bold (approximately 18.66 CSS px)3:14.5:1
Non-text UI components and essential graphics3:13:1
Focus indicators3:13:1

All text MUST reach at least 4.5:1 against its actual background in every theme, regardless of size or weight. This includes placeholders and disabled control labels. Non-text UI components, focus indicators, and essential graphics MUST reach at least 3:1 (SC 1.4.11).

Why: A single 4.5:1 threshold for all text is simpler to test, holds up when users scale text, and avoids arguing about what counts as large.

W3C rounds 14pt to approximately 18.5 CSS px and WebAIM to 18.66 CSS px; both are roundings of 14pt at 1.333px per point. For a WCAG-only audit, evaluate the point size rather than the rounded pixel value. These size boundaries never lower crefft’s 4.5:1 text requirement.

Contrast ratios are thresholds and MUST NOT be rounded before the comparison: a computed 4.499:1 fails the 4.5:1 text requirement, and 2.995:1 fails the 3:1 non-text requirement. Round for display only, never for the pass or fail decision.

Why: 18 CSS px regular text is 13.5pt, far below WCAG’s 18pt large-scale boundary. #949494 on white computes 3.03:1, which meets WCAG’s minimum for large text but fails crefft’s 4.5:1 requirement for text of any size. #959595 on white computes 2.9953:1, which displays as 3.0:1 yet fails even the 3:1 non-text threshold.

/* DON'T: 3.03:1 is insufficient for text, even a large heading */
h1 { color: #949494; background: #ffffff; }

/* DO: 4.54:1 meets the all-text requirement */
h1 { color: #767676; background: #ffffff; }

Color Independence:

Information MUST NOT be conveyed through color alone:

<!-- ✗ DON'T: Color only -->
<span style="color: red">Error</span>

<!-- ✓ DO: Color + icon + text -->
<span class="error">
  <svg aria-hidden="true"><!-- error icon --></svg>
  Error: Invalid email address
</span>

Text Resizing:

  • Text MUST be resizable up to 200% without loss of functionality
  • Use relative units (rem, em) not pixels for text
  • Test with browser zoom at 200%

2. Operable

2.1 Keyboard Accessible

All functionality MUST be available via keyboard:

/* Visible focus indicators */
:focus-visible {
  outline: 2px solid var(--interactive-default);
  outline-offset: 2px;
}

/* Never remove focus outline without replacement */
/* ✗ DON'T */
:focus { outline: none; }

/* ✓ DO */
:focus:not(:focus-visible) { outline: none; }
:focus-visible { outline: 2px solid var(--interactive-default); }

Keyboard Navigation Requirements:

KeyExpected Behavior
TabMove focus to next interactive element
Shift+TabMove focus to previous interactive element
EnterActivate buttons, links, submit forms
SpaceActivate buttons, toggle checkboxes
EscapeClose modals, dropdowns, cancel operations
Arrow keysNavigate within widgets (tabs, menus, etc.)

Focus Management:

// Return focus after modal close
function closeModal(modal, triggerElement) {
  modal.close();
  triggerElement.focus();
}

// Focus trap for modals
function trapFocus(container) {
  const focusable = container.querySelectorAll(
    'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  container.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;

    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  });
}

2.4 Navigable

Skip Links:

<body>
  <a href="#main-content" class="skip-link">Skip to main content</a>
  <nav>...</nav>
  <main id="main-content">...</main>
</body>
.skip-link {
  position: absolute;
  left: -9999px;
  z-index: 999;
  padding: 1rem;
  background: var(--surface-card);
}

.skip-link:focus {
  left: 0;
}

Descriptive Headings:

  • Each page MUST have one <h1>
  • Headings MUST be hierarchical (no skipping levels)
  • Headings MUST describe the content that follows
<!-- ✓ DO: Proper hierarchy -->
<h1>Dashboard</h1>
  <h2>Recent Activity</h2>
  <h2>Statistics</h2>
    <h3>Monthly Overview</h3>
    <h3>Yearly Trends</h3>

<!-- ✗ DON'T: Skipped levels -->
<h1>Dashboard</h1>
  <h3>Recent Activity</h3>  <!-- Skipped h2 -->

Descriptive Link Text:

<!-- ✗ DON'T: Vague link text -->
<a href="/docs">Click here</a>
<a href="/more">Read more</a>

<!-- ✓ DO: Descriptive link text -->
<a href="/docs">View documentation</a>
<a href="/more">Read more about accessibility</a>

3. Understandable

3.1 Readable

Language Declaration:

<html lang="en">
  <!-- Page content in English -->
  <p>Welcome to our site.</p>

  <!-- Content in different language -->
  <p lang="es">Bienvenido a nuestro sitio.</p>
</html>

3.2 Predictable

  • Navigation MUST be consistent across pages
  • Components MUST behave consistently
  • Focus changes MUST NOT cause unexpected context changes

3.3 Input Assistance

Error Identification:

<div class="form-field">
  <label for="email">Email</label>
  <input
    type="email"
    id="email"
    aria-invalid="true"
    aria-describedby="email-error"
  />
  <p id="email-error" class="error" role="alert">
    Please enter a valid email address (e.g., name@example.com)
  </p>
</div>

Error Prevention:

  • Confirm destructive actions
  • Allow users to review before submission
  • Provide clear instructions before complex forms

4. Robust

4.1 Compatible

Valid HTML:

  • Use valid, semantic HTML
  • Close all tags properly
  • Use unique IDs

ARIA Usage:

ARIA supplements HTML—it does not replace it:

<!-- ✗ DON'T: ARIA instead of semantic HTML -->
<div role="button" tabindex="0">Click me</div>

<!-- ✓ DO: Semantic HTML -->
<button>Click me</button>

<!-- ✓ DO: ARIA when needed -->
<button aria-expanded="false" aria-controls="menu">
  Menu
</button>
<ul id="menu" hidden>...</ul>

Common ARIA Patterns

Live Regions

Announce dynamic content to screen readers:

<!-- Polite: Announced after current speech -->
<div aria-live="polite" aria-atomic="true">
  3 items in cart
</div>

<!-- Assertive: Announced immediately -->
<div aria-live="assertive" role="alert">
  Error: Connection lost
</div>

Expanded/Collapsed

<button aria-expanded="false" aria-controls="details">
  Show details
</button>
<div id="details" hidden>
  <!-- Details content -->
</div>
button.addEventListener('click', () => {
  const expanded = button.getAttribute('aria-expanded') === 'true';
  button.setAttribute('aria-expanded', !expanded);
  details.hidden = expanded;
});

Tabs

<div class="tabs">
  <div role="tablist" aria-label="Settings tabs">
    <button
      role="tab"
      id="tab-1"
      aria-selected="true"
      aria-controls="panel-1"
    >
      General
    </button>
    <button
      role="tab"
      id="tab-2"
      aria-selected="false"
      aria-controls="panel-2"
      tabindex="-1"
    >
      Security
    </button>
  </div>
  <div
    role="tabpanel"
    id="panel-1"
    aria-labelledby="tab-1"
  >
    <!-- General settings -->
  </div>
  <div
    role="tabpanel"
    id="panel-2"
    aria-labelledby="tab-2"
    hidden
  >
    <!-- Security settings -->
  </div>
</div>
<dialog
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc"
>
  <h2 id="dialog-title">Confirm Delete</h2>
  <p id="dialog-desc">
    Are you sure you want to delete this item? This cannot be undone.
  </p>
  <button>Cancel</button>
  <button>Delete</button>
</dialog>

Reduced Motion

Respect user preferences for reduced motion:

/* Default animations */
.animate {
  transition: transform 0.3s ease;
}

/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Provide alternatives for motion-based features:

const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

if (prefersReducedMotion) {
  // Show static fallback instead of animation
  showStaticVersion();
} else {
  playAnimation();
}

Testing

Automated Testing

Use automated tools for baseline testing:

ToolPurpose
axe-coreRuntime accessibility testing
LighthouseAudit tool in Chrome DevTools
WAVEBrowser extension for visual testing
pa11yCI/CD integration

Automated testing catches ~30% of issues. Manual testing is required.

Manual Testing

Keyboard Testing

  1. Unplug your mouse
  2. Navigate entire interface with Tab, Shift+Tab, Enter, Space, Escape
  3. Verify focus is always visible
  4. Verify all functionality is reachable
  5. Verify modals trap focus

Screen Reader Testing

Test with at least one screen reader:

PlatformScreen Reader
macOSVoiceOver (built-in)
WindowsNVDA (free) or JAWS
iOSVoiceOver (built-in)
AndroidTalkBack (built-in)

What to test:

  1. All content is announced
  2. Headings structure is logical
  3. Form labels are associated
  4. Images have meaningful alt text
  5. Dynamic content is announced
  6. Error messages are announced

Visual Testing

  1. Zoom to 200% - is content still usable?
  2. Test with high contrast mode
  3. Test with inverted colors
  4. Test with grayscale (color blindness simulation)

Checklist

Before Development

  • Color contrast verified (4.5:1 for all text, 3:1 for non-text UI and focus) in every theme
  • Focus states designed
  • Error states designed with clear messages
  • Keyboard interaction patterns defined

During Development

  • Semantic HTML used
  • ARIA attributes added where needed
  • Focus management implemented
  • Skip links included
  • Form labels associated

Before Launch

  • Automated testing passes (axe, Lighthouse)
  • Manual keyboard testing complete
  • Screen reader testing complete
  • Zoom testing complete (200%)
  • Reduced motion respected

Resources

Standards

Tools


See Also