Accessibility with Tailwind

Tailwind CSS · Lesson 24 of 27 7 min read Updated

An accessible website works for everyone: people who use a keyboard instead of a mouse, blind people using a screen reader, people with low vision who zoom in or use high-contrast mode, people with tremors tapping on small phones, and people who get dizzy from motion. In India, the Rights of Persons with Disabilities Act, 2016 and government web guidelines also expect public services to be accessible.

Tailwind does not make a page accessible by itself; good HTML does most of the work (see Accessibility Basics). But Tailwind gives you utilities and variants that make the styling side easy. In this lesson you will add screen-reader text and skip links, design clear focus styles, check contrast and touch target sizes, respect motion and contrast settings, and style from ARIA state.

Text for screen readers only: sr-only

Some information is obvious to sighted users but missing for screen-reader users, such as an icon-only button. sr-only hides text visually while keeping it in the page for screen readers. not-sr-only undoes it.

HTML
<button class="rounded-full p-2 hover:bg-slate-100">
  <svg aria-hidden="true" class="size-5">...</svg>
  <span class="sr-only">Close menu</span>
</button>

The SVG is decorative, so aria-hidden="true" hides it from screen readers, and the sr-only text gives the button its name. Never use hidden for this; hidden removes text from screen readers too.

Keyboard users press Tab to move through links and buttons. On a page with a long navigation menu, they must tab through every link before reaching the content. A skip link is the first link on the page; it is invisible until it receives focus, then lets them jump straight to the main content. sr-only focus:not-sr-only is exactly that behaviour:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Skip link and focus</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="bg-white text-slate-900">
  <a href="#main" class="sr-only rounded-md bg-sky-700 text-white focus:not-sr-only focus:absolute focus:top-3 focus:left-3 focus:z-50 focus:px-4 focus:py-2">Skip to main content</a>
  <header class="flex items-center gap-4 border-b border-slate-200 p-4">
    <span class="font-bold">Jaipur Municipal Services</span>
    <nav class="flex gap-3 text-sm">
      <a href="#" class="rounded px-1 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Water bill</a>
      <a href="#" class="rounded px-1 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Property tax</a>
      <a href="#" class="rounded px-1 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Complaints</a>
    </nav>
    <button class="ms-auto rounded-full p-2 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-700">
      <svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 fill-none stroke-current stroke-2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
      <span class="sr-only">Open menu</span>
    </button>
  </header>
  <main id="main" class="p-4">
    <h1 class="text-2xl font-bold">Pay your water bill</h1>
  </main>
</body>
</html>

Click inside the result, then press Tab once: the skip link appears in the top corner. Its padding is written as focus:px-4 focus:py-2 because not-sr-only resets padding to 0; the focus versions come later in the CSS and win. Press Tab again to see each link's outline. A good focus style is clearly visible on every background: 2px or thicker, with an offset, in a colour that contrasts with both the element and the page.

Colour contrast

Text must contrast enough with its background. The Web Content Accessibility Guidelines (WCAG) at level AA ask for a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (about 24px, or 19px bold) and for important parts of controls, such as an input's border and focus indicators.

With Tailwind's palette, a reliable rule is: shade 600 or darker on white for normal text (700 is safer for small text), and white text on 600 or darker backgrounds. Light shades such as gray-400 text look elegant but usually fail. Browser DevTools show the contrast ratio when you inspect a text element, so check your main colour pairs once.

Touch targets

Fingers are bigger than mouse pointers. WCAG 2.2 at level AA requires interactive targets of at least 24 by 24 CSS pixels (with some exceptions), and 44 by 44 is a comfortable size for phones. A small icon becomes an easy target with padding: an icon of size-5 in a button with p-3 gives a 44px target. Tailwind 4.1's pointer-coarse: variant applies only on touch screens, so you can give extra room where it helps: py-2 pointer-coarse:py-3.

Respecting user settings

Operating systems let people choose how they want content shown. Tailwind has a variant for each setting:

VariantMatches when the user...
motion-reduce:, motion-safe:has, or has not, asked for reduced motion
contrast-more:, contrast-less:asked for more, or less, contrast
forced-colors:uses forced colours, such as Windows high-contrast mode
inverted-colors:has inverted the screen colours
pointer-coarse:, pointer-fine:uses a touch screen, or a mouse or trackpad
print:is printing the page
HTML
<div class="rounded-xl border border-slate-200 p-4 contrast-more:border-2 contrast-more:border-slate-900 motion-safe:transition hover:shadow-md">
  ...
</div>

In forced-colours mode, the browser replaces your colours with the user's system colours and drops backgrounds and shadows. Borders and outlines survive, so a card that relies only on a shadow becomes invisible. A transparent border (border border-transparent) keeps a visible edge in that mode without changing the normal design.

ARIA state and styles that match

When a component's state is stored in ARIA attributes, style from those attributes. Then what screen readers announce and what sighted users see always agree. Here is an accessible accordion:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Accessible accordion</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="p-6">
  <div class="max-w-md divide-y divide-slate-200 rounded-xl border border-slate-200">
    <h3>
      <button id="q1" aria-expanded="false" aria-controls="a1" class="group flex w-full items-center justify-between p-4 text-start font-medium hover:bg-slate-50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-700">
        What documents do I need for admission?
        <svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 shrink-0 fill-none stroke-current stroke-2 motion-safe:transition-transform group-aria-expanded:rotate-180"><path d="m6 9 6 6 6-6"/></svg>
      </button>
    </h3>
    <div id="a1" role="region" aria-labelledby="q1" hidden class="p-4 pt-0 text-slate-600">
      Your Class 12 marksheet, an ID proof such as Aadhaar, and two passport-size photos.
    </div>
  </div>
  <script>
    const button = document.getElementById('q1');
    const panel = document.getElementById('a1');
    button.addEventListener('click', () => {
      const open = button.getAttribute('aria-expanded') === 'true';
      button.setAttribute('aria-expanded', String(!open));
      panel.hidden = open;
    });
  </script>
</body>
</html>

The script only flips aria-expanded and the hidden attribute. The arrow's rotation comes from group-aria-expanded:rotate-180, so the visual state can never drift away from what screen readers hear. The rotation is animated only with motion-safe:. For a simpler built-in alternative, the <details> element (see Details, Summary & Dialog) needs no script at all, and Tailwind's open: variant styles it.

Common mistakes

  • Removing focus outlines. Never use outline-none alone. Replace the default with a visible focus-visible: style, and prefer outline-hidden when you draw focus with rings.
  • Icon buttons without names. Add sr-only text or an aria-label.
  • Light grey text. Check contrast; text-slate-400 on white fails for body text.
  • Reordering with CSS. order-* and flex-row-reverse change only the visual order; keyboard and screen-reader order follow the HTML.
  • Hover-only information. Touch screens and keyboards cannot hover.

Key points

  • Good HTML comes first; Tailwind helps with the styling side of accessibility.
  • sr-only hides text visually but keeps it for screen readers; sr-only focus:not-sr-only makes a skip link.
  • Give every interactive element a clear focus-visible: style; use outline-hidden, not outline-none, when replacing outlines with rings.
  • Aim for WCAG AA contrast (4.5:1 for normal text) and touch targets of at least 24px, ideally 44px.
  • Variants like motion-reduce:, contrast-more:, forced-colors: and pointer-coarse: respect user settings.
  • Style from ARIA attributes (aria-expanded:, group-aria-expanded:) so visuals match what assistive technology announces.

Exercise

Make a "Like" icon button for a recipe site run by Meera in Kochi. It should show a heart SVG that is hidden from screen readers, have screen-reader text "Like this recipe", be at least 44px square, show a clear focus outline for keyboard users, and use aria-pressed to show a filled red heart when pressed. Write the small script that toggles aria-pressed.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Like button</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="p-8">
  <button id="like" aria-pressed="false" class="group rounded-full p-3 text-slate-500 hover:bg-rose-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-700 aria-pressed:text-rose-600">
    <svg aria-hidden="true" viewBox="0 0 24 24" class="size-5 fill-none stroke-current stroke-2 group-aria-pressed:fill-current">
      <path d="M12 21s-7-4.5-9.5-9A5.5 5.5 0 0 1 12 6a5.5 5.5 0 0 1 9.5 6c-2.5 4.5-9.5 9-9.5 9z"/>
    </svg>
    <span class="sr-only">Like this recipe</span>
  </button>
  <script>
    const like = document.getElementById('like');
    like.addEventListener('click', () => {
      like.setAttribute('aria-pressed', String(like.getAttribute('aria-pressed') !== 'true'));
    });
  </script>
</body>
</html>

p-3 around a 20px icon makes a 44px target, sr-only gives the button its name, and aria-pressed both tells screen readers the state and drives aria-pressed:text-rose-600 and group-aria-pressed:fill-current.

Quick Quiz

1. What does sr-only do?

2. Which classes turn a link into a skip link that only appears on keyboard focus?

3. Why is outline-hidden preferred over outline-none when you replace the outline with a ring?

4. What minimum contrast ratio does WCAG level AA require for normal-sized text?

Learn Tailwind CSS with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call