Accessibility with Tailwind CSS
Accessibility with Tailwind CSS
Building accessible websites ensures that all users, including those with disabilities, can use your applications effectively. Tailwind CSS provides utilities that make implementing accessibility features straightforward. This lesson covers essential accessibility techniques using Tailwind.
Why Accessibility Matters
Accessibility Benefits
- Inclusive Design: Makes your site usable by people with visual, auditory, motor, or cognitive disabilities
- Legal Compliance: Many countries require web accessibility (WCAG, ADA, Section 508)
- Better UX for Everyone: Accessibility improvements benefit all users
- SEO Benefits: Semantic HTML and proper structure improve search rankings
- Wider Audience: ~15% of the world's population has some form of disability
Screen Reader Only Content (sr-only)
The sr-only utility visually hides content while keeping it accessible to screen readers. Use it for providing additional context that visual users don't need.
Basic sr-only Usage
<!-- Skip navigation link for keyboard users -->
<a href="#main-content" class="
sr-only
focus:not-sr-only
focus:absolute focus:top-4 focus:left-4
focus:z-50
focus:px-4 focus:py-2
focus:bg-blue-600 focus:text-white
focus:rounded-md
">
Skip to main content
</a>
<!-- Icon button with sr-only label -->
<button class="p-2 hover:bg-gray-100 rounded-md">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 3.5a1.5 1.5 0 013 0V4a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-.5a1.5 1.5 0 000 3h.5a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-.5a1.5 1.5 0 00-3 0v.5a1 1 0 01-1 1H6a1 1 0 01-1-1v-3a1 1 0 00-1-1h-.5a1.5 1.5 0 010-3H4a1 1 0 001-1V6a1 1 0 011-1h3a1 1 0 001-1v-.5z"/>
</svg>
<span class="sr-only">Open settings</span>
</button>
<!-- Close button with icon -->
<button type="button" class="absolute top-4 right-4 p-1 hover:bg-gray-100 rounded">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span class="sr-only">Close modal</span>
</button>
When to Use sr-only
- Icon-only buttons without visible text labels
- Skip navigation links
- Additional context for form inputs
- Status messages that update dynamically
- Decorative images that need description
Keyboard Navigation with focus-visible
Keyboard users rely on focus indicators to know where they are on the page. Tailwind's focus and focus-visible utilities help create clear focus states.
Focus Styles for Interactive Elements
<!-- Basic focus ring -->
<button class="
px-4 py-2
bg-blue-600 text-white
rounded-md
focus:outline-none
focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
">
Click Me
</button>
<!-- focus-visible: only shows focus ring for keyboard users -->
<a href="#" class="
text-blue-600 underline
focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:rounded
">
Learn More
</a>
<!-- Custom focus styles for links -->
<nav class="space-x-6">
<a href="#" class="
text-gray-700 hover:text-gray-900
focus:outline-none
focus:underline focus:decoration-2 focus:decoration-blue-500
focus:underline-offset-4
">
Home
</a>
<a href="#" class="
text-gray-700 hover:text-gray-900
focus:outline-none
focus:underline focus:decoration-2 focus:decoration-blue-500
focus:underline-offset-4
">
About
</a>
</nav>
<!-- Focus within: style parent when child is focused -->
<div class="
p-4 border-2 border-gray-200 rounded-lg
focus-within:border-blue-500
focus-within:ring-2 focus-within:ring-blue-200
">
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<input
type="email"
id="email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none"
>
</div>
Focus vs Focus-Visible
- focus: Shows focus styles for both mouse clicks and keyboard navigation
- focus-visible: Shows focus styles only for keyboard navigation (recommended for better UX)
- focus-within: Styles parent element when any child is focused
ARIA Attributes with Tailwind
ARIA (Accessible Rich Internet Applications) attributes provide semantic meaning to assistive technologies. Tailwind supports styling based on ARIA states.
ARIA-based Styling
<!-- Button with aria-pressed -->
<button
type="button"
aria-pressed="false"
class="
px-4 py-2
bg-gray-200 text-gray-700
rounded-md
aria-pressed:bg-blue-600 aria-pressed:text-white
hover:bg-gray-300
aria-pressed:hover:bg-blue-700
"
onclick="this.setAttribute('aria-pressed', this.getAttribute('aria-pressed') === 'true' ? 'false' : 'true')"
>
Toggle
</button>
<!-- Tabs with ARIA -->
<div role="tablist" class="flex border-b border-gray-200">
<button
role="tab"
aria-selected="true"
aria-controls="panel-1"
class="
px-4 py-2
border-b-2 border-transparent
text-gray-600
aria-selected:border-blue-600 aria-selected:text-blue-600
hover:text-gray-900
focus:outline-none focus:ring-2 focus:ring-blue-500
"
>
Tab 1
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-2"
class="
px-4 py-2
border-b-2 border-transparent
text-gray-600
aria-selected:border-blue-600 aria-selected:text-blue-600
hover:text-gray-900
focus:outline-none focus:ring-2 focus:ring-blue-500
"
>
Tab 2
</button>
</div>
<!-- Disclosure with aria-expanded -->
<button
type="button"
aria-expanded="false"
aria-controls="content"
class="
flex items-center justify-between
w-full px-4 py-3
text-left
bg-gray-100
hover:bg-gray-200
rounded-lg
"
>
<span>Click to expand</span>
<svg
class="
w-5 h-5
transition-transform
aria-expanded:rotate-180
"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
Common ARIA Patterns
<!-- Modal dialog -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
class="
fixed inset-0 z-50
flex items-center justify-center
bg-black/50
"
>
<div class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<h2 id="modal-title" class="text-xl font-bold mb-4">
Modal Title
</h2>
<p class="text-gray-600 mb-6">
Modal content goes here...
</p>
<div class="flex gap-3 justify-end">
<button class="px-4 py-2 bg-gray-200 rounded-md">
Cancel
</button>
<button class="px-4 py-2 bg-blue-600 text-white rounded-md">
Confirm
</button>
</div>
</div>
</div>
<!-- Alert message -->
<div
role="alert"
aria-live="assertive"
class="
p-4
bg-red-100 border border-red-400
text-red-700
rounded-md
"
>
<strong class="font-semibold">Error!</strong>
<span class="block sm:inline">Something went wrong.</span>
</div>
<!-- Loading spinner -->
<div
role="status"
aria-live="polite"
aria-label="Loading"
class="flex items-center gap-3"
>
<svg class="animate-spin h-5 w-5 text-blue-600" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
<span class="sr-only">Loading...</span>
</div>
Color Contrast Considerations
Proper color contrast is essential for users with visual impairments. WCAG 2.1 requires minimum contrast ratios for text.
WCAG Contrast Requirements
- Level AA (minimum): 4.5:1 for normal text, 3:1 for large text (18pt+ or 14pt+ bold)
- Level AAA (enhanced): 7:1 for normal text, 4.5:1 for large text
- Non-text: 3:1 for UI components and graphical objects
Good and Bad Contrast Examples
<!-- BAD: Low contrast (2.5:1) -->
<p class="text-gray-400 bg-white">
This text is hard to read for many users
</p>
<!-- GOOD: High contrast (10.4:1) -->
<p class="text-gray-900 bg-white">
This text meets WCAG AAA standards
</p>
<!-- BAD: Button with poor contrast -->
<button class="bg-yellow-300 text-yellow-100 px-4 py-2 rounded">
Submit
</button>
<!-- GOOD: Button with good contrast -->
<button class="bg-blue-600 text-white px-4 py-2 rounded">
Submit
</button>
<!-- Safe color combinations in Tailwind -->
<div class="space-y-4">
<!-- Dark text on light backgrounds -->
<div class="bg-gray-100 text-gray-900 p-4 rounded">
Gray 900 on Gray 100 (13.6:1) ✓
</div>
<!-- Light text on dark backgrounds -->
<div class="bg-gray-800 text-white p-4 rounded">
White on Gray 800 (10.8:1) ✓
</div>
<!-- Colored backgrounds -->
<div class="bg-blue-600 text-white p-4 rounded">
White on Blue 600 (4.6:1) ✓
</div>
<div class="bg-green-700 text-white p-4 rounded">
White on Green 700 (5.2:1) ✓
</div>
</div>
Tools for Checking Contrast
- WebAIM Contrast Checker: https://webaim.org/resources/contrastchecker/
- Browser DevTools: Chrome/Edge have built-in contrast checkers
- Tailwind Color Generator: Tools that show WCAG-compliant color pairs
- Figma Plugins: Stark, Contrast, A11y - Color Contrast Checker
Motion and Animation Preferences
Some users experience motion sickness or vestibular disorders triggered by animations. Tailwind provides utilities to respect user motion preferences.
motion-reduce and motion-safe
<!-- Disable animation for users who prefer reduced motion -->
<button class="
px-4 py-2
bg-blue-600 text-white
rounded-md
transform
hover:scale-105
motion-reduce:transform-none
motion-reduce:hover:scale-100
transition-transform
motion-reduce:transition-none
">
Hover Me
</button>
<!-- Only animate for users who allow motion -->
<div class="
motion-safe:animate-spin
motion-reduce:animate-none
">
<svg class="w-8 h-8" viewBox="0 0 24 24">
<!-- Spinner icon -->
</svg>
</div>
<!-- Fade in animation with motion preference -->
<div class="
opacity-0
motion-safe:animate-fade-in
motion-reduce:opacity-100
">
Content
</div>
<!-- Slide animation with fallback -->
<div class="
transform -translate-x-full
motion-safe:transition-transform motion-safe:translate-x-0
motion-reduce:translate-x-0
">
Sliding content
</div>
Custom Animation with prefers-reduced-motion
<!-- In your CSS or tailwind.config.js -->
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.5s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.animate-fade-in {
animation: none;
opacity: 1;
transform: translateY(0);
}
}
<!-- In HTML -->
<div class="animate-fade-in">
Content fades in smoothly (or appears instantly if motion is reduced)
</div>
Semantic HTML Importance
Using semantic HTML elements is the foundation of accessibility. Tailwind styles can be applied to any element, so choose the right one.
Semantic vs Non-semantic
<!-- BAD: Using divs for everything -->
<div class="cursor-pointer text-blue-600 underline" onclick="navigate()">
Go to page
</div>
<!-- GOOD: Using semantic elements -->
<a href="/page" class="text-blue-600 underline hover:text-blue-800">
Go to page
</a>
<!-- BAD: Non-semantic structure -->
<div class="text-2xl font-bold mb-4">Page Title</div>
<div class="mb-8">
<div class="mb-2">Article content...</div>
</div>
<!-- GOOD: Semantic structure -->
<article>
<h1 class="text-2xl font-bold mb-4">Page Title</h1>
<p class="mb-2">Article content...</p>
</article>
<!-- Use appropriate semantic elements -->
<header class="bg-white shadow">...</header>
<nav class="flex space-x-4">...</nav>
<main class="container mx-auto">...</main>
<aside class="w-64 bg-gray-50">...</aside>
<footer class="bg-gray-900 text-white">...</footer>
Accessible Forms
Forms are critical for user interaction and must be accessible to all users.
Complete Accessible Form Example
<form class="space-y-6 max-w-md mx-auto">
<!-- Text input with label -->
<div>
<label
for="name"
class="block text-sm font-medium text-gray-700 mb-2"
>
Full Name
<span class="text-red-600" aria-label="required">*</span>
</label>
<input
type="text"
id="name"
name="name"
required
aria-required="true"
aria-describedby="name-error"
class="
w-full px-3 py-2
border border-gray-300 rounded-md
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
disabled:bg-gray-100 disabled:cursor-not-allowed
"
>
<p id="name-error" class="text-sm text-red-600 mt-1 hidden">
Please enter your name
</p>
</div>
<!-- Email with helper text -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<input
type="email"
id="email"
name="email"
aria-describedby="email-help"
class="
w-full px-3 py-2
border border-gray-300 rounded-md
focus:outline-none focus:ring-2 focus:ring-blue-500
"
>
<p id="email-help" class="text-sm text-gray-500 mt-1">
We'll never share your email
</p>
</div>
<!-- Radio group -->
<fieldset>
<legend class="block text-sm font-medium text-gray-700 mb-2">
Subscription Type
</legend>
<div class="space-y-2">
<div class="flex items-center">
<input
type="radio"
id="free"
name="subscription"
value="free"
class="
w-4 h-4
text-blue-600
focus:ring-2 focus:ring-blue-500
"
>
<label for="free" class="ml-2 text-gray-700">
Free
</label>
</div>
<div class="flex items-center">
<input
type="radio"
id="premium"
name="subscription"
value="premium"
class="
w-4 h-4
text-blue-600
focus:ring-2 focus:ring-blue-500
"
>
<label for="premium" class="ml-2 text-gray-700">
Premium
</label>
</div>
</div>
</fieldset>
<!-- Checkbox -->
<div class="flex items-start">
<input
type="checkbox"
id="terms"
name="terms"
required
aria-required="true"
class="
w-4 h-4 mt-1
text-blue-600
focus:ring-2 focus:ring-blue-500
"
>
<label for="terms" class="ml-2 text-sm text-gray-700">
I agree to the
<a href="/terms" class="text-blue-600 hover:underline">
terms and conditions
</a>
</label>
</div>
<!-- Submit button -->
<button
type="submit"
class="
w-full px-4 py-2
bg-blue-600 text-white
font-medium rounded-md
hover:bg-blue-700
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
"
>
Submit
</button>
</form>
Skip Navigation Links
Skip links allow keyboard users to bypass repetitive navigation and jump directly to main content.
Implementing Skip Links
<!-- Place at the very top of your body -->
<a
href="#main-content"
class="
sr-only
focus:not-sr-only
focus:absolute
focus:top-4 focus:left-4
focus:z-50
focus:px-4 focus:py-2
focus:bg-blue-600 focus:text-white
focus:font-medium focus:rounded-md
focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2
"
>
Skip to main content
</a>
<header>
<!-- Navigation -->
</header>
<main id="main-content" tabindex="-1">
<!-- Main content starts here -->
</main>
Practice Exercise 1: Audit for Accessibility
Take an existing Tailwind component and audit it for accessibility:
- Check color contrast ratios (use browser DevTools)
- Ensure all interactive elements are keyboard accessible
- Add appropriate ARIA attributes
- Test with a screen reader (NVDA, JAWS, or VoiceOver)
- Verify motion respects prefers-reduced-motion
Practice Exercise 2: Build an Accessible Modal
Create a modal dialog with complete accessibility:
- Proper ARIA roles and attributes
- Focus trap (focus stays within modal)
- ESC key closes modal
- Focus returns to trigger element on close
- Keyboard navigation for buttons
Practice Exercise 3: Accessible Data Table
Build a data table with:
- Proper table structure (thead, tbody, th, td)
- Column headers with scope="col"
- Caption describing the table
- Sortable columns with ARIA attributes
- Keyboard navigation between cells
Summary
Building accessible interfaces with Tailwind CSS involves:
- sr-only: Hide content visually while keeping it accessible
- Focus styles: Use focus-visible for keyboard navigation indicators
- ARIA attributes: Provide semantic meaning with proper ARIA roles and states
- Color contrast: Ensure WCAG AA (4.5:1) or AAA (7:1) contrast ratios
- Motion preferences: Respect prefers-reduced-motion with motion-reduce utilities
- Semantic HTML: Use the right elements (nav, main, article, etc.)
- Accessible forms: Label inputs, provide error messages, use fieldset for groups
- Skip links: Allow keyboard users to bypass navigation
In the next lesson, we'll explore Tailwind CSS v4 and its modern features including the new CSS-first configuration system.