Tailwind CSS

Accessibility with Tailwind CSS

20 min Lesson 32 of 35

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:

  1. Check color contrast ratios (use browser DevTools)
  2. Ensure all interactive elements are keyboard accessible
  3. Add appropriate ARIA attributes
  4. Test with a screen reader (NVDA, JAWS, or VoiceOver)
  5. 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.