Accessibility
Stacks.js is designed with accessibility in mind. This guide covers best practices for building applications that work for everyone.
WCAG Compliance
Accessibility Levels
| Level | Description | Target |
|---|---|---|
| A | Minimum accessibility | Required |
| AA | Industry standard | Recommended |
| AAA | Enhanced accessibility | Aspirational |
Stacks default components are designed for WCAG 2.1 Level AA compliance.
Semantic HTML
Use Proper Elements
<!-- BAD: Divs for everything -->
<divclass="onclick="Submit</div
<divclass="Welcome</div
<divclass="...</div
<!-- GOOD: Semantic elements -->
<buttontype="Submit</button
<h1Welcome</h1
<nav...</nav
Document Structure
<x-layout
<header
<navaria-label="
<x-nav-menu
</nav
</header
<mainid="
<h1Page Title</h1
<article
<h2Section Title</h2
<pContent...</p
</article
</main
<asidearia-label="
<x-sidebar
</aside
<footer
<x-footer
</footer
</x-layout
Heading Hierarchy
<!-- GOOD: Logical heading order -->
<h1Main Title</h1
<h2Section 1</h2
<h3Subsection 1.1</h3
<h3Subsection 1.2</h3
<h2Section 2</h2
<h3Subsection 2.1</h3
<!-- BAD: Skipped levels -->
<h1Main Title</h1
<h3Skipped h2!</h3
ARIA Attributes
Roles and Labels
<!-- Search form -->
<formrole="aria-label="
<x-input
type="search"
name="q"
aria-label="Search query"
placeholder="Search..."
/>
<x-buttontype="aria-label="
<x-iconname="aria-hidden="
</x-button
</form
<!-- Navigation -->
<navaria-label="
<olrole="
<li<ahref="Home</a</li
<li<ahref="Products</a</li
<liaria-current="Current Product</li
</ol
</nav
Live Regions
<!-- Announce dynamic content changes -->
<div
aria-live="polite"
aria-atomic="true"
class="sr-only"
>
{{ notification }}
</div
<!-- Urgent announcements -->
<div
role="alert"
aria-live="assertive"
>
{{ errorMessage }}
</div
Expanded/Collapsed State
<button
aria-expanded=""
aria-controls="dropdown-menu"
="toggle"
>
Menu
<x-icon:name="isOpen?'chevron-up':'chevron-down'aria-hidden="
</button
<ul
id="dropdown-menu"
:hidden="!isOpen"
role="menu"
>
<lirole="<ahref="Profile</a</li
<lirole="<ahref="Settings</a</li
</ul
Keyboard Navigation
Focus Management
<!-- Skip link -->
<ahref="class="
Skip to main content
</a
<!-- Focus visible styling (included in defaults) -->
<style>
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
</style
Focus Trapping (Modals)
<x-modal
:open="showModal"
="showModal = false"
trap-focus
>
<x-modal-header
<h2id="Modal Title</h2
</x-modal-header
<x-modal-body
<pModal content...</p
</x-modal-body
<x-modal-footer
<x-button@click="showModal=falseClose</x-button
</x-modal-footer
</x-modal
Keyboard Shortcuts
// Provide keyboard shortcuts for common actions
document.addEventListener('keydown', (e) => {
// Escape to close modals
if (e.key === 'Escape') {
closeModal()
}
// Ctrl/Cmd + K for search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault()
openSearch()
}
})
Forms
Labels and Descriptions
<x-form-group
<x-labelfor="requiredEmail Address</x-label
<x-input
id="email"
type="email"
name="email"
aria-describedby="email-hint email-error"
:aria-invalid="errors.email ? 'true' : 'false'"
/>
<x-hintid="We'll never share your email.</x-hint
<x-errorid=":show="errors
{{ errors.email }}
</x-error
</x-form-group
Error Handling
<form@submit="handleSubmitnovalidate
<!-- Error summary at top -->
<x-alert
v-if="hasErrors"
type="error"
role="alert"
aria-live="assertive"
>
<h2Please correct the following errors:</h2
<ul
@foreach(errorsaserror
<li<ahref="error{{ error.message }}</a</li
</ul
</x-alert
<!-- Form fields... -->
</form
Required Fields
<!-- Visual and programmatic indication -->
<x-labelfor="required
Name <spanaria-hidden="class="*</span
</x-label
<x-input
id="name"
name="name"
required
aria-required="true"
/>
<!-- Legend for required fields -->
<pclass="
<spanaria-hidden="*</span Required fields
</p
Images and Media
Image Alt Text
<!-- Informative images -->
<img
src="/images/chart.png"
alt="Sales increased 25% from Q1 to Q2 2024"
/>
<!-- Decorative images -->
<imgsrc="alt="role="
<!-- Complex images with description -->
<figure
<img
src="/images/org-chart.png"
alt="Organization chart"
aria-describedby="org-description"
/>
<figcaptionid="
Detailed description of the organizational structure...
</figcaption
</figure
Video Accessibility
<x-video
src="/videos/tutorial.mp4"
poster="/images/tutorial-poster.jpg"
>
<track
kind="captions"
src="/captions/tutorial-en.vtt"
srclang="en"
label="English"
default
/>
<track
kind="captions"
src="/captions/tutorial-es.vtt"
srclang="es"
label="Spanish"
/>
<track
kind="descriptions"
src="/descriptions/tutorial.vtt"
srclang="en"
label="Audio descriptions"
/>
</x-video
Audio Content
<!-- Provide transcripts -->
<audiocontrolsaria-describedby="
<sourcesrc="type="
</audio
<detailsid="
<summaryView transcript</summary
<div
Full transcript of the audio content...
</div
</details
Color and Contrast
Color Contrast Requirements
| Element | Minimum Ratio (AA) | Enhanced Ratio (AAA) |
|---|---|---|
| Normal text | 4.5:1 | 7:1 |
| Large text (18px+) | 3:1 | 4.5:1 |
| UI components | 3:1 | 3:1 |
Don't Rely on Color Alone
<!-- BAD: Color only indication -->
<spanclass="Error occurred</span
<!-- GOOD: Color + icon + text -->
<spanclass="
<x-iconname="aria-hidden="
Error: Invalid email format
</span
<!-- GOOD: Charts with patterns -->
<x-chart
:data="data"
use-patterns
accessible-colors
/>
Dark Mode Considerations
// config/theme.ts
export default {
colors: {
light: {
text: '#1a1a1a', // High contrast on white
background: '#ffffff',
},
dark: {
text: '#f5f5f5', // High contrast on dark
background: '#1a1a1a',
}
}
}
Tables
Accessible Data Tables
<table
<captionMonthly Sales Report - 2024</caption
<thead
<tr
<thscope="Month</th
<thscope="Revenue</th
<thscope="Growth</th
</tr
</thead
<tbody
<tr
<thscope="January</th
<td$50,000</td
<td+5%</td
</tr
<tr
<thscope="February</th
<td$55,000</td
<td+10%</td
</tr
</tbody
</table
Complex Tables
<table
<captionQuarterly Revenue by Region</caption
<thead
<tr
<td</td
<thscope="id="Q1</th
<thscope="id="Q2</th
</tr
</thead
<tbody
<tr
<thscope="id="North</th
<tdheaders="$100k</td
<tdheaders="$120k</td
</tr
<tr
<thscope="id="South</th
<tdheaders="$80k</td
<tdheaders="$90k</td
</tr
</tbody
</table
Testing Accessibility
Automated Testing
# Run accessibility audit
buddy a11y:check
# In tests
import { axe } from '@stacksjs/testing'
test('page is accessible', async () => {
const page = await render('/dashboard')
const results = await axe(page)
expect(results.violations).toHaveLength(0)
})
Manual Testing Checklist
- All content accessible via keyboard
- Focus order is logical
- Focus is visible
- Screen reader announces content correctly
- Color contrast meets requirements
- Forms have proper labels
- Error messages are clear and announced
- Images have appropriate alt text
- Videos have captions
- No content flashes more than 3 times/second
Screen Reader Testing
Test with:
- macOS: VoiceOver (built-in)
- Windows: NVDA (free), JAWS
- Mobile: VoiceOver (iOS), TalkBack (Android)
# Enable VoiceOver on macOS
# Cmd + F5
# Common VoiceOver shortcuts
# VO + Right Arrow: Move forward
# VO + Left Arrow: Move backward
# VO + Space: Activate element
Utility Classes
Screen Reader Only
<!-- Visually hidden but accessible to screen readers -->
<spanclass="Loading, please wait</span
<!-- CSS definition (included in defaults) -->
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.not-sr-only {
position: static;
width: auto;
height: auto;
padding: 0;
margin: 0;
overflow: visible;
clip: auto;
white-space: normal;
}
</style
Focus Styles
<!-- Visible focus on keyboard navigation -->
<style>
.focus-visible:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.focus-within:focus-within {
ring: 2px solid var(--color-primary);
}
</style
Component Accessibility
All built-in STX components are accessible by default:
| Component | Accessibility Features |
|---|---|
<x-button> | Proper role, keyboard support |
<x-modal> | Focus trap, escape to close, aria-modal |
<x-dropdown> | Arrow key navigation, proper roles |
<x-tabs> | Arrow key switching, proper roles |
<x-accordion> | Expand/collapse with Enter/Space |
<x-alert> | role="alert" for important messages |
<x-form-group> | Associated labels and descriptions |