Accessibility Requirements
This guide documents our established patterns for ensuring accessibility, using real examples from our codebase. These requirements apply to all our frontend patterns:
For testing accessibility requirements, see our Testing Patterns guide.
Core Requirements
Section titled “Core Requirements”We follow WCAG 2.1 Level AA standards, focusing on four main principles:
-
Perceivable
- Text alternatives for non-text content
- Captions and alternatives for media
- Content adaptable and distinguishable
- Sufficient color contrast
-
Operable
- Keyboard accessible
- Enough time to read/use content
- No seizure-inducing content
- Easy navigation
-
Understandable
- Readable text
- Predictable operation
- Input assistance
-
Robust
- Compatible with assistive technologies
Implementation Patterns
Section titled “Implementation Patterns”Component Accessibility
Section titled “Component Accessibility”When implementing components following our Component Architecture, ensure:
---interface Props { title: string; description: string; date: Date;}---
<article class="card"> <h2>{title}</h2> <p>{description}</p> <footer> <time datetime={date.toISOString()}>{date.toLocaleDateString()}</time> </footer></article>Form Accessibility
Section titled “Form Accessibility”When implementing forms following our Forms and User Input patterns, ensure:
export function NewsletterForm() { return ( <form role="form" aria-labelledby="form-title" noValidate > <h2 id="form-title">Subscribe to Newsletter</h2>
<div role="alert" aria-live="polite"> {error() && <p mode="critical" class="text-primary">{error()}</p>} </div>
<label for="email">Email Address</label> <input id="email" type="email" aria-required="true" aria-invalid={hasError()} aria-describedby={hasError() ? "email-error" : undefined} />
{hasError() && ( <p mode="critical" id="email-error" class="text-primary"> Please enter a valid email address </p> )}
<button type="submit" disabled={isSubmitting()} aria-busy={isSubmitting()} > {isSubmitting() ? "Submitting..." : "Subscribe"} </button> </form> );}Data Fetching Accessibility
Section titled “Data Fetching Accessibility”When implementing data fetching following our Data Fetching and GraphQL Operations patterns, ensure:
export function ArticleList() { return ( <div role="feed" aria-busy={isLoading()}> {isLoading() && ( <div role="status"> <span class="sr-only">Loading articles...</span> <LoadingSpinner /> </div> )}
{error() && ( <div role="alert"> <p>Failed to load articles: {error()}</p> <button onClick={retry}>Try Again</button> </div> )}
{articles().map(article => ( <article role="article"> <h2>{article.title}</h2> <p>{article.excerpt}</p> </article> ))} </div> );}Semantic HTML
Section titled “Semantic HTML”Use semantic HTML elements to provide clear structure and meaning:
---// ✅ Do: Use semantic elements---
<article class="card"> <h2>{title}</h2> <p>{description}</p> <footer> <time datetime={date}>{formattedDate}</time> </footer></article>
--- // ❌ Don't: Use generic divs ---<div class="card"> <div class="title">{title}</div> <div class="description">{description}</div> <div class="footer"> <div class="date">{formattedDate}</div> </div></div>ARIA Attributes
Section titled “ARIA Attributes”Use ARIA attributes when semantic HTML isn’t sufficient:
export const Tabs: Component<TabsProps> = props => { return ( <div role="tablist" aria-label={props.label}> {props.tabs.map((tab, index) => ( <button role="tab" aria-selected={index === props.activeTab} aria-controls={`panel-${tab.id}`} id={`tab-${tab.id}`} > {tab.label} </button> ))} {props.tabs.map((tab, index) => ( <div role="tabpanel" aria-labelledby={`tab-${tab.id}`} id={`panel-${tab.id}`} hidden={index !== props.activeTab} > {tab.content} </div> ))} </div> );};Focus Management
Section titled “Focus Management”Implement proper focus management for interactive elements:
export const Modal: Component<ModalProps> = props => { let dialogRef: HTMLDialogElement | undefined;
onMount(() => { if (props.isOpen) { dialogRef?.showModal(); // Focus first focusable element const firstFocusable = dialogRef?.querySelector( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ) as HTMLElement; firstFocusable?.focus(); } });
return ( <dialog ref={dialogRef} class="modal" aria-labelledby="modal-title" aria-describedby="modal-description" > <h2 id="modal-title">{props.title}</h2> <div id="modal-description">{props.children}</div> <button onClick={() => props.onClose()} aria-label="Close modal"> × </button> </dialog> );};Color and Contrast
Section titled “Color and Contrast”Ensure sufficient color contrast and don’t rely solely on color:
export const Alert: Component<AlertProps> = props => { return ( <div role="alert" class="alert" data-mode={props.type === "error" ? "critical" : "success"} > {/* Include icon for non-color status indication */} <span class="sr-only">{props.type}:</span> <Icon type={props.type} aria-hidden="true" /> {props.children} </div> );};Testing Tools
Section titled “Testing Tools”Automated Testing
Section titled “Automated Testing”We use multiple tools for automated accessibility testing:
- Deque Axe Monitoring
Enterprise-level accessibility monitoring Request access through your team lead to view reports
Provides:
- Automated scans of production sites
- Detailed violation reports
- Trend analysis
- Compliance tracking
- Storybook Accessibility Addon
export default { addons: ["@storybook/addon-a11y"],};- Jest Axe Testing
import { render } from "@testing-library/solid";import { axe, toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);
describe("Button", () => { it("should have no accessibility violations", async () => { const { container } = render(() => ( <Button>Click me</Button> ));
const results = await axe(container); expect(results).toHaveNoViolations(); });});🤔 Why Multiple Testing Tools?
- Deque Axe provides enterprise-level monitoring and reporting
- Storybook catches issues during development
- Jest integration ensures CI/CD coverage
- Combined approach for maximum coverage
Manual Testing
Section titled “Manual Testing”Use these tools for manual accessibility testing:
-
Keyboard Navigation
- Tab through all interactive elements
- Verify focus indicators
- Test keyboard shortcuts
- Check focus trapping in modals
-
Screen Readers
- Test with VoiceOver (macOS)
- Verify ARIA attributes
- Check content order
- Test dynamic updates
-
Browser Extensions
- Axe DevTools
- WAVE Evaluation Tool
- Color Contrast Analyzer
Best Practices
Section titled “Best Practices”-
Content Structure
- Use proper heading hierarchy
- Provide skip links
- Group related elements
- Use landmarks appropriately
-
Interactive Elements
- Ensure keyboard access
- Provide focus indicators
- Use appropriate ARIA
- Handle touch inputs
-
Media Content
- Add alt text to images
- Provide video captions
- Include audio transcripts
- Consider reduced motion
-
Dynamic Content
- Announce updates
- Manage focus
- Handle loading states
- Provide status messages
Anti-Patterns to Avoid
Section titled “Anti-Patterns to Avoid”❌ Don’t: Remove focus outlines without alternatives
/* Bad: Removing focus indication */*:focus { outline: none;}✅ Do: Style focus states appropriately
/* Good: Custom focus styles */*:focus-visible { outline: 2px solid #0066cc; outline-offset: 2px;}❌ Don’t: Use non-semantic elements for interaction
// Bad: Using div for button<div onClick={handleClick}>Click me</div>✅ Do: Use semantic elements
// Good: Using button element<button onClick={handleClick}>Click me</button>Decision Checklist
Section titled “Decision Checklist”When implementing features, ask:
-
Semantic Structure
- Using appropriate HTML elements?
- Proper heading hierarchy?
- ARIA attributes when needed?
- Clear content structure?
-
Keyboard Interaction
- All features keyboard accessible?
- Visible focus indicators?
- Logical tab order?
- Keyboard shortcuts documented?
-
Visual Design
- Sufficient color contrast?
- Not relying solely on color?
- Text readable at 200% zoom?
- Responsive layout?
-
Dynamic Content
- Screen reader announcements?
- Focus management?
- Loading state indicators?
- Error messages clear?