Files
singular-particular-space/skills/commissioning-skill/fetched-skills/ux-design/references/refui-accessibility-depth.md
JL Kruger 5422131782 Initial commit — Singular Particular Space v1
Homepage (site/index.html): integration-v14 promoted, Writings section
integrated with 33 pieces clustered by type (stories/essays/miscellany),
Writings welcome lightbox, content frame at 98% opacity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 12:09:22 +02:00

11 KiB

Accessibility Deep Dive

Comprehensive WCAG 2.1 AA compliance checklist with practical implementation guidance.

Accessibility Philosophy

Accessibility isn't just compliance—it improves UX for everyone:

  • Keyboard navigation helps power users
  • Good contrast helps in bright sunlight
  • Clear focus states help everyone understand what's selected
  • Proper headings help screen readers AND SEO

WCAG 2.1 AA Checklist

Perceivable

Users must be able to perceive content.

1.1 Text Alternatives

All images need alt text:

<!-- Decorative image (no alt) -->
<img src="divider.svg" alt="" role="presentation">

<!-- Informative image -->
<img src="chart.png" alt="Sales increased 40% from Q1 to Q2">

<!-- Functional image (button/link) -->
<button>
  <img src="search.svg" alt="Search">
</button>

<!-- Complex image (needs long description) -->
<figure>
  <img src="diagram.png" alt="System architecture overview" aria-describedby="diagram-desc">
  <figcaption id="diagram-desc">Detailed description of the system architecture...</figcaption>
</figure>

Icons:

<!-- Icon with visible text (icon is decorative) -->
<button>
  <svg aria-hidden="true">...</svg>
  <span>Settings</span>
</button>

<!-- Icon-only button -->
<button aria-label="Settings">
  <svg aria-hidden="true">...</svg>
</button>

1.3 Adaptable

Semantic HTML structure:

<!-- Use semantic elements -->
<header>...</header>
<nav>...</nav>
<main>
  <article>
    <h1>Page Title</h1>
    <section>
      <h2>Section Title</h2>
    </section>
  </article>
</main>
<footer>...</footer>

<!-- Not this -->
<div class="header">...</div>
<div class="nav">...</div>

Heading hierarchy:

  • One <h1> per page
  • Don't skip levels (h1 → h3)
  • Headings describe content structure

Form labels:

<!-- Explicit label -->
<label for="email">Email</label>
<input id="email" type="email">

<!-- Implicit label -->
<label>
  Email
  <input type="email">
</label>

<!-- Hidden label (for visual designs without labels) -->
<label for="search" class="sr-only">Search</label>
<input id="search" type="search" placeholder="Search...">

1.4 Distinguishable

Color contrast requirements:

Content Type Minimum Ratio Tool
Normal text (<18px) 4.5:1 WebAIM Contrast Checker
Large text (≥18px or ≥14px bold) 3:1
UI components & graphics 3:1

Common contrast fixes:

/* Too light - fails */
.text-light { color: #9ca3af; } /* gray-400: 3.1:1 on white */

/* Passes AA */
.text-muted { color: #6b7280; } /* gray-500: 4.6:1 on white */

/* Passes AAA */
.text-strong { color: #374151; } /* gray-700: 9.1:1 on white */

Don't rely on color alone:

<!-- Bad: only color indicates error -->
<input class="border-red-500">

<!-- Good: color + icon + text -->
<input class="border-red-500" aria-invalid="true" aria-describedby="error">
<p id="error" class="text-red-600">
  <svg aria-hidden="true">⚠️</svg>
  Email is required
</p>

Text resize:

  • Content must be readable at 200% zoom
  • Use relative units (rem, em) not px for text
  • Test by zooming browser to 200%

Operable

Users must be able to operate the interface.

2.1 Keyboard Accessible

All functionality must work with keyboard:

Key Expected Behavior
Tab Move to next focusable element
Shift+Tab Move to previous focusable element
Enter Activate links, buttons
Space Activate buttons, toggle checkboxes
Arrows Navigate within components (tabs, menus, radios)
Escape Close modals, dropdowns, cancel actions

Focus must be visible:

/* Don't remove focus outlines */
:focus {
  outline: none; /* ❌ Never do this without replacement */
}

/* Do provide visible focus */
:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

/* Or use ring utility */
.focusable:focus-visible {
  @apply ring-2 ring-blue-500 ring-offset-2;
}

Keyboard traps:

  • Modal dialogs should trap focus inside
  • But must have a way to exit (Escape key, close button)
// Focus trap for modals
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef();

  useEffect(() => {
    if (isOpen) {
      // Focus first focusable element
      const firstFocusable = modalRef.current.querySelector('button, input, a');
      firstFocusable?.focus();

      // Trap focus inside
      const handleTab = (e) => {
        if (e.key === 'Tab') {
          // ... trap logic
        }
        if (e.key === 'Escape') {
          onClose();
        }
      };

      document.addEventListener('keydown', handleTab);
      return () => document.removeEventListener('keydown', handleTab);
    }
  }, [isOpen]);

  // Return focus on close
}

Skip links:

<body>
  <a href="#main-content" class="sr-only focus:not-sr-only">
    Skip to main content
  </a>
  <nav>...</nav>
  <main id="main-content">...</main>
</body>

2.4 Navigable

Page titles:

  • Unique, descriptive page titles
  • Format: Page Name | Site Name

Focus order:

  • Must follow logical reading order
  • Don't use positive tabindex values (messes up order)
  • Only use tabindex="0" (make focusable) or tabindex="-1" (programmatically focusable)

Link purpose:

<!-- Bad -->
<a href="/article">Click here</a>
<a href="/article">Read more</a>

<!-- Good -->
<a href="/article">Read more about accessibility best practices</a>

<!-- Or with context -->
<a href="/article" aria-describedby="article-title">Read more</a>

Understandable

Users must be able to understand content and operation.

3.1 Readable

Language declaration:

<html lang="en">
  <body>
    <p>This is English.</p>
    <p lang="fr">Ceci est français.</p>
  </body>
</html>

3.2 Predictable

Consistent navigation:

  • Same navigation in same location across pages
  • Same elements behave the same way

No unexpected changes:

  • Form inputs don't auto-submit on change
  • No unexpected pop-ups
  • Focus doesn't move unexpectedly
<!-- Bad: changes page on select -->
<select onchange="window.location = this.value">...</select>

<!-- Good: requires explicit action -->
<select id="region">...</select>
<button onclick="navigate()">Go</button>

3.3 Input Assistance

Error identification:

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

Required fields:

<label for="name">
  Name <span aria-hidden="true">*</span>
  <span class="sr-only">(required)</span>
</label>
<input id="name" required aria-required="true">

Error prevention for critical actions:

  • Confirm destructive actions
  • Allow review before submission
  • Provide undo capability

Robust

Content must work with current and future technologies.

4.1 Compatible

Valid HTML:

  • Unique IDs
  • Complete start/end tags
  • Proper nesting

ARIA usage:

<!-- If you use ARIA, use it correctly -->

<!-- Roles -->
<div role="button" tabindex="0" onclick="...">Fake Button</div>
<!-- Better: just use <button> -->

<!-- States -->
<button aria-pressed="true">Bold</button>
<button aria-expanded="false" aria-controls="menu">Menu</button>

<!-- Live regions -->
<div aria-live="polite" aria-atomic="true">
  <!-- Screen reader announces changes here -->
</div>

Focus Management

When to Manage Focus

Scenario Focus Action
Modal opens Focus first element inside modal
Modal closes Return focus to trigger element
Error occurs Focus error message or first invalid field
New content loads Focus heading or first new element
Item deleted Focus previous/next item or container

Implementation

// Store trigger reference
const triggerRef = useRef();

function openModal() {
  triggerRef.current = document.activeElement;
  setIsOpen(true);
}

function closeModal() {
  setIsOpen(false);
  // Return focus after state update
  setTimeout(() => triggerRef.current?.focus(), 0);
}

Roving Tabindex (for component groups)

// Tab panels, menu items, radio groups
function Tabs({ tabs }) {
  const [activeIndex, setActiveIndex] = useState(0);

  const handleKeyDown = (e) => {
    if (e.key === 'ArrowRight') {
      setActiveIndex((activeIndex + 1) % tabs.length);
    }
    if (e.key === 'ArrowLeft') {
      setActiveIndex((activeIndex - 1 + tabs.length) % tabs.length);
    }
  };

  return (
    <div role="tablist" onKeyDown={handleKeyDown}>
      {tabs.map((tab, i) => (
        <button
          key={tab.id}
          role="tab"
          tabIndex={i === activeIndex ? 0 : -1}
          aria-selected={i === activeIndex}
          ref={i === activeIndex ? (el) => el?.focus() : null}
        >
          {tab.label}
        </button>
      ))}
    </div>
  );
}

Screen Reader Considerations

Announce Dynamic Changes

<!-- Live region for status messages -->
<div aria-live="polite" class="sr-only" id="status"></div>

<script>
function showSuccess(message) {
  document.getElementById('status').textContent = message;
}
</script>

Hide Decorative Content

<!-- Hidden from screen readers -->
<svg aria-hidden="true">...</svg>
<span aria-hidden="true"></span>

Provide Context

<!-- Ambiguous button -->
<button>Delete</button>

<!-- Clear button -->
<button aria-label="Delete comment by John">Delete</button>

<!-- Or use aria-describedby -->
<button aria-describedby="comment-123-author">Delete</button>
<span id="comment-123-author" class="sr-only">comment by John</span>

Testing Tools

Automated Testing

Tool What It Catches
axe DevTools ~30% of WCAG issues
WAVE Similar to axe, visual overlay
Lighthouse Basic accessibility audit
ESLint a11y plugin Catches issues in JSX

Manual Testing Required

Automated tools miss ~70% of issues. Manual testing needed for:

  • Keyboard navigation flow
  • Screen reader experience
  • Focus management
  • Meaningful alt text
  • Logical heading structure

Testing Checklist

  • Navigate entire page with keyboard only
  • Test with screen reader (VoiceOver, NVDA)
  • Check color contrast with tool
  • Zoom to 200% and verify usability
  • Test with high contrast mode
  • Verify focus indicators visible
  • Check heading structure with outline tool
  • Run axe DevTools audit
  • Test forms with validation errors

Common Fixes Quick Reference

Issue Fix
Missing alt text Add descriptive alt or alt="" for decorative
Low contrast Use gray-600+ for text on white
Missing focus style Add focus-visible ring/outline
Click-only interaction Add keyboard handler + focusability
Missing form labels Add <label> with for attribute
Heading skip Use h1→h2→h3 in order
Color-only indicator Add icon/text alongside color
Modal focus trap Trap focus, allow Escape to close
Auto-playing media Add pause control, don't autoplay
Motion Respect prefers-reduced-motion