Accessibility with Tailwind
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.
<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.
Skip links and focus styles
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:
<!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:
| Variant | Matches 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 |
<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:
<!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-nonealone. Replace the default with a visiblefocus-visible:style, and preferoutline-hiddenwhen you draw focus with rings. - Icon buttons without names. Add
sr-onlytext or anaria-label. - Light grey text. Check contrast;
text-slate-400on white fails for body text. - Reordering with CSS.
order-*andflex-row-reversechange 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-onlyhides text visually but keeps it for screen readers;sr-only focus:not-sr-onlymakes a skip link.- Give every interactive element a clear
focus-visible:style; useoutline-hidden, notoutline-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:andpointer-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
<!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?
sr-only uses a set of properties that make text invisible on screen while screen readers still read it.
2. Which classes turn a link into a skip link that only appears on keyboard focus?
sr-only keeps the link in the tab order and readable; focus:not-sr-only shows it when it receives focus.
3. Why is outline-hidden preferred over outline-none when you replace the outline with a ring?
Rings are box shadows, which forced-colours mode removes; outline-hidden keeps a transparent outline that becomes visible there.
4. What minimum contrast ratio does WCAG level AA require for normal-sized text?
Normal text needs 4.5:1; large text and important parts of controls need 3:1.
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
