Skip to content

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.

We follow WCAG 2.1 Level AA standards, focusing on four main principles:

  1. Perceivable

    • Text alternatives for non-text content
    • Captions and alternatives for media
    • Content adaptable and distinguishable
    • Sufficient color contrast
  2. Operable

    • Keyboard accessible
    • Enough time to read/use content
    • No seizure-inducing content
    • Easy navigation
  3. Understandable

    • Readable text
    • Predictable operation
    • Input assistance
  4. Robust

    • Compatible with assistive technologies

When implementing components following our Component Architecture, ensure:

components/card/card.astro
---
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>

When implementing forms following our Forms and User Input patterns, ensure:

components/newsletterForm/newsletterForm.tsx
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>
);
}

When implementing data fetching following our Data Fetching and GraphQL Operations patterns, ensure:

components/articleList/articleList.tsx
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>
);
}

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>

Use ARIA attributes when semantic HTML isn’t sufficient:

components/tabs/tabs.tsx
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>
);
};

Implement proper focus management for interactive elements:

components/modal/modal.tsx
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>
);
};

Ensure sufficient color contrast and don’t rely solely on color:

components/alert/alert.tsx
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>
);
};

We use multiple tools for automated accessibility testing:

  1. 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
  1. Storybook Accessibility Addon
.storybook/main.ts
export default {
addons: ["@storybook/addon-a11y"],
};
  1. Jest Axe Testing
components/button/button.spec.tsx
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

Use these tools for manual accessibility testing:

  1. Keyboard Navigation

    • Tab through all interactive elements
    • Verify focus indicators
    • Test keyboard shortcuts
    • Check focus trapping in modals
  2. Screen Readers

    • Test with VoiceOver (macOS)
    • Verify ARIA attributes
    • Check content order
    • Test dynamic updates
  3. Browser Extensions

    • Axe DevTools
    • WAVE Evaluation Tool
    • Color Contrast Analyzer
  1. Content Structure

    • Use proper heading hierarchy
    • Provide skip links
    • Group related elements
    • Use landmarks appropriately
  2. Interactive Elements

    • Ensure keyboard access
    • Provide focus indicators
    • Use appropriate ARIA
    • Handle touch inputs
  3. Media Content

    • Add alt text to images
    • Provide video captions
    • Include audio transcripts
    • Consider reduced motion
  4. Dynamic Content

    • Announce updates
    • Manage focus
    • Handle loading states
    • Provide status messages

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>

When implementing features, ask:

  1. Semantic Structure

    • Using appropriate HTML elements?
    • Proper heading hierarchy?
    • ARIA attributes when needed?
    • Clear content structure?
  2. Keyboard Interaction

    • All features keyboard accessible?
    • Visible focus indicators?
    • Logical tab order?
    • Keyboard shortcuts documented?
  3. Visual Design

    • Sufficient color contrast?
    • Not relying solely on color?
    • Text readable at 200% zoom?
    • Responsive layout?
  4. Dynamic Content

    • Screen reader announcements?
    • Focus management?
    • Loading state indicators?
    • Error messages clear?