ADR-019: Accessibility Patterns and Standards
Status
Section titled “Status”Accepted
Context
Section titled “Context”The Astro Performance Starter targets WCAG 2.1 Level AA compliance across all components and pages. As the codebase has grown, we need a consolidated reference for accessibility patterns to ensure consistency and maintainability.
This ADR consolidates all accessibility architectural decisions and patterns into a single source of truth.
Compliance Target
Section titled “Compliance Target”WCAG 2.1 Level AA - All components and pages must meet or exceed this standard.
Key principles:
- Perceivable: Information must be presentable to users in ways they can perceive
- Operable: UI components must be operable by all users
- Understandable: Information and UI operation must be understandable
- Robust: Content must be robust enough for assistive technologies
Decision
Section titled “Decision”Adopt the patterns catalogued below as the binding accessibility standard for every page and component in the template. This record is deliberately a pattern catalogue rather than a single choice: the decision is that these patterns — and only these — are the sanctioned way to meet the compliance target above. (Section added 2026-07-05; the record predated the template’s required Decision heading.)
Accessibility Patterns
Section titled “Accessibility Patterns”1. Decorative Content
Section titled “1. Decorative Content”Emojis
Section titled “Emojis”Pattern: All decorative emojis must be hidden from screen readers.
<!-- ✅ Correct: Decorative emoji with adjacent text --><span aria-hidden="true">📧</span><h3>Email</h3>
<!-- ❌ Incorrect: Screen reader announces "envelope" redundantly --><span>📧</span><h3>Email</h3>
<!-- ✅ Correct: Meaningful emoji (rare case) --><span role="img" aria-label="Warning">⚠️</span>When to use:
- Emoji serves purely visual enhancement
- Adjacent text conveys the same meaning
- Emoji is decorative/illustrative
When NOT to use:
- Emoji is the only indicator of meaning (use
role="img"+aria-label) - Emoji conveys unique information not in text
Implementation: See ADR 018 for contact page example.
Pattern: Decorative icons should be hidden; functional icons need labels.
<!-- ✅ Decorative icon with adjacent text --><img src={icon.src} alt="" aria-hidden="true" /><span>GitHub</span>
<!-- ✅ Functional icon-only button --><button aria-label="Close menu"> <img src={closeIcon.src} alt="" aria-hidden="true" /></button>
<!-- ❌ Icon-only button without label --><button> <img src={closeIcon.src} alt="Close" /></button>Guidelines:
- Use
alt=""for decorative images (notalt="icon") - Add
aria-hidden="true"to prevent redundant announcements - Icon-only interactive elements MUST have
aria-label - Prefer text + icon over icon-only when possible
Images
Section titled “Images”Pattern: Use appropriate alt text based on image purpose.
<!-- ✅ Informative image --><Image src={screenshot} alt="Dashboard showing performance metrics" />
<!-- ✅ Decorative image --><Image src={background} alt="" />
<!-- ✅ Functional image (link/button) --><a href="/about"> <Image src={avatar} alt="About Jane Doe" /></a>
<!-- ❌ Missing alt text --><Image src={photo} />Alt text guidelines:
- Informative: Describe content and function
- Decorative: Use
alt=""(empty string, not omitted) - Functional: Describe destination/action
- Complex: Provide long description via adjacent text or
aria-describedby
2. ARIA Roles
Section titled “2. ARIA Roles”Badge Component
Section titled “Badge Component”Pattern: Use semantic roles based on badge purpose.
<!-- ✅ Decorative/Informational badge --><Badge role="presentation">Available for projects</Badge><Badge role="presentation">Remote-friendly</Badge>
<!-- ✅ Live status update --><Badge role="status">Online now</Badge><Badge role="status">Processing...</Badge>
<!-- ✅ Default informational (no role) --><Badge>TypeScript</Badge>Decision tree:
Is the badge announcing a live status change?├─ YES → role="status"│ Examples: "Online now", "Processing", "3 unread"└─ NO → Is it purely decorative? ├─ YES → role="presentation" │ Examples: "Remote-friendly", "Quick response" └─ NO → No role (default) Examples: Technology tags, categoriesRationale: role="status" causes screen readers to announce changes, which is only appropriate for live updates.
Interactive Components
Section titled “Interactive Components”Pattern: Ensure all interactive elements have accessible names.
<!-- ✅ SocialLink with context --><SocialLink platform="github" href="https://github.com/user" aria-label="Visit my GitHub profile (opens in new tab)"/>
<!-- ✅ Button with explicit label --><Button href="/contact" aria-label="Contact us"> Get In Touch</Button>
<!-- ✅ Form input with associated label --><label for="email">Email Address</label><input id="email" type="email" required />Requirements:
- All interactive elements must have accessible names
- Use
aria-labelwhen visible text is insufficient - Associate form inputs with
<label>elements - Provide context for links that open in new tabs
3. Focus Management
Section titled “3. Focus Management”Skip Links
Section titled “Skip Links”Pattern: All pages must include skip links for keyboard navigation.
<!-- ✅ Skip link in BaseLayout --><SkipLink href="#main-content">Skip to main content</SkipLink>
<main id="main-content"> <!-- Page content --></main>Requirements:
- Skip link must be first focusable element
- Target must be a valid
idon the page - Skip link can be visually hidden until focused
- Must be keyboard accessible (Tab key)
Implementation: src/components/a11y/SkipLink.astro
Focus Order
Section titled “Focus Order”Pattern: Focus order must follow logical reading order (left-to-right, top-to-bottom).
<!-- ✅ Natural DOM order --><div class="grid md:grid-cols-3 gap-8"> <div>First item</div> <div>Second item</div> <div>Third item</div></div>
<!-- ❌ CSS reordering that breaks tab order --><div class="flex"> <div class="order-2">Visually first</div> <div class="order-1">Visually second</div></div>Guidelines:
- Use natural DOM order for tab navigation
- Avoid CSS
orderproperty that conflicts with DOM order - Test with keyboard-only navigation
- Document complex focus flows with HTML comments
Focus Indicators
Section titled “Focus Indicators”Pattern: All focusable elements must have visible focus indicators.
<!-- ✅ Tailwind focus utilities --><button class="focus:outline-hidden focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"> Click me</button>
<!-- ✅ Custom focus-ring utility (from @utility in src/styles/global.css) --><a href="/about" class="focus-ring"> About</a>Requirements:
- Focus indicators must have 3:1 contrast ratio with background
- Never use
outline: nonewithout replacement - Use
focus-visible:for keyboard-only indicators - Test with keyboard navigation
4. Forms & Validation
Section titled “4. Forms & Validation”Form Labels
Section titled “Form Labels”Pattern: All form inputs must have associated labels.
<!-- ✅ Explicit label association --><label for="name">Full Name</label><input id="name" type="text" required />
<!-- ✅ Implicit label (less preferred) --><label> Email Address <input type="email" required /></label>
<!-- ✅ Hidden label with aria-label --><input type="search" aria-label="Search blog posts" placeholder="Search..."/>Requirements:
- Use explicit
for/idassociation when possible - Provide visible labels (not just placeholders)
- Use
aria-labelonly when visible label isn’t feasible - Group related inputs with
<fieldset>and<legend>
Error Messages
Section titled “Error Messages”Pattern: Error messages must be programmatically associated with inputs.
<!-- ✅ Error with aria-describedby --><label for="email">Email</label><input id="email" type="email" aria-invalid="true" aria-describedby="email-error"/><span id="email-error" role="alert"> Please enter a valid email address</span>
<!-- ✅ Live region for dynamic errors --><div role="alert" aria-live="polite"> {errorMessage}</div>Requirements:
- Use
aria-invalid="true"on invalid inputs - Associate errors with
aria-describedby - Use
role="alert"for error messages - Provide clear, actionable error text
5. Semantic HTML
Section titled “5. Semantic HTML”Landmarks
Section titled “Landmarks”Pattern: Use semantic HTML5 elements for page structure.
<!-- ✅ Semantic structure --><header> <nav aria-label="Main navigation"> <!-- Navigation links --> </nav></header>
<main id="main-content"> <article> <h1>Page Title</h1> <!-- Content --> </article></main>
<footer> <!-- Footer content --></footer>
<!-- ❌ Non-semantic divs --><div class="header"> <div class="nav"> <!-- Navigation --> </div></div>Requirements:
- Use
<header>,<nav>,<main>,<footer>,<aside> - One
<main>landmark per page - Use
aria-labelto distinguish multiple<nav>elements - Avoid redundant ARIA roles on semantic elements
Headings
Section titled “Headings”Pattern: Use proper heading hierarchy (h1-h6).
<!-- ✅ Proper hierarchy --><h1>Page Title</h1> <h2>Section</h2> <h3>Subsection</h3> <h2>Another Section</h2>
<!-- ❌ Skipped levels --><h1>Page Title</h1> <h3>Section</h3> <!-- Skipped h2 -->Requirements:
- One
<h1>per page - Don’t skip heading levels
- Use headings for structure, not styling
- Use CSS for visual hierarchy
6. Color & Contrast
Section titled “6. Color & Contrast”Contrast Ratios
Section titled “Contrast Ratios”Pattern: All text must meet WCAG AA contrast requirements.
Requirements:
- Normal text: 4.5:1 minimum contrast ratio
- Large text (18pt+ or 14pt+ bold): 3:1 minimum
- UI components: 3:1 for interactive elements
- Disabled elements: Exempt from contrast requirements
Validation: Use pnpm run design:validate to check token contrast.
Color Alone
Section titled “Color Alone”Pattern: Don’t rely on color alone to convey information.
<!-- ✅ Color + icon + text --><div class="text-red-600"> <span aria-hidden="true">❌</span> <span>Error: Invalid input</span></div>
<!-- ❌ Color only --><div class="text-red-600"> Invalid input</div>Requirements:
- Use icons, text, or patterns in addition to color
- Provide text alternatives for color-coded information
- Test with grayscale/colorblind simulators
7. Motion & Animation
Section titled “7. Motion & Animation”Reduced Motion
Section titled “Reduced Motion”Pattern: Respect prefers-reduced-motion user preference.
/* ✅ Respect user preference */.animate { animation: slide-in 0.3s ease-out;}
@media (prefers-reduced-motion: reduce) { .animate { animation: none; }}Tailwind utilities:
<!-- ✅ Motion-safe utilities --><div class="motion-safe:transition-transform motion-reduce:transition-none"> Content</div>Requirements:
- All animations must respect
prefers-reduced-motion - Use Tailwind
motion-safe:andmotion-reduce:utilities - Provide instant state changes for reduced motion
- Never use animation for critical information
Testing Strategy
Section titled “Testing Strategy”Manual Testing
Section titled “Manual Testing”-
Keyboard Navigation
- Tab through all interactive elements
- Verify focus indicators are visible
- Test Shift+Tab (reverse navigation)
- Ensure no keyboard traps
-
Screen Readers
- NVDA (Windows + Firefox)
- JAWS (Windows + Chrome/Edge)
- VoiceOver (macOS + Safari)
- TalkBack (Android + Chrome)
-
Browser DevTools
- Chrome Lighthouse accessibility audit
- Firefox Accessibility Inspector
- axe DevTools browser extension
Automated Testing
Section titled “Automated Testing”# Run accessibility testspnpm run test:a11y
# Validate design token contrastpnpm run design:validate
# Full quality checkpnpm run qualityTools:
- Playwright with
@axe-core/playwrightfor automated testing - Lighthouse CI for performance + accessibility scores
- Custom scripts for contrast validation
Implementation Examples
Section titled “Implementation Examples”Contact Page (ADR 018)
Section titled “Contact Page (ADR 018)”See ADR 018: Contact Page Accessibility for a complete implementation example covering:
- Decorative emoji patterns
- Semantic badge roles
- Focus order documentation
Component Library
Section titled “Component Library”All atomic components follow these patterns:
src/components/atoms/Badge.astro- Semantic rolessrc/components/atoms/SocialLink.astro- Accessible linkssrc/components/atoms/Button.astro- Focus indicatorssrc/components/a11y/SkipLink.astro- Skip navigation
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Single Source of Truth: One document for all accessibility patterns
- Consistency: Clear guidelines prevent accessibility debt
- Onboarding: New developers have comprehensive reference
- Maintainability: Easier to update patterns in one place
- Compliance: Systematic approach ensures WCAG AA compliance
Negative
Section titled “Negative”- Initial Overhead: Requires upfront documentation effort
- Maintenance: Must keep ADR updated as patterns evolve
Neutral
Section titled “Neutral”- Living Document: This ADR will be updated as new patterns emerge
- Not Exhaustive: Covers common patterns; edge cases may need additional documentation
Related ADRs
Section titled “Related ADRs”- ADR 000: Starter Decisions - WCAG AA compliance requirement
- ADR 018: Contact Page Accessibility - Implementation example
References
Section titled “References”WCAG Guidelines
Section titled “WCAG Guidelines”ARIA Specifications
Section titled “ARIA Specifications”Testing Resources
Section titled “Testing Resources”Enforcement
Section titled “Enforcement”- Testable consequences:
- TC-1: every key page (the seven top-level routes in
e2e/a11y-axe.spec.ts) passes the automated axe WCAG 2.1 A/AA scan with zero serious or critical violations, measured under reduced motion. (The axe integration shipped 2026-08-13 — this section predated it by a month; the hand-written@a11ystructural tests covered the gap.)
- TC-1: every key page (the seven top-level routes in
- Checks:
- TC-1 →
@a11yPlaywright suite in CI (status: block, pre-existing gate)
- TC-1 →
- Not machine-checkable: pattern selection for novel UI (which sanctioned pattern applies) is a judgment; axe coverage is necessary, not sufficient, for WCAG AA.
- Graduation log: (empty at creation; entries added when a check changes status)
Date: 2025-10-01 (footer backfilled 2026-07-05 from git history; this record predates the footer convention)
Participants: Template maintainers
Outcome: Accepted