Accessible, Global, Printable
Tailwind CSS Intermediate/Advanced
Chapter 9 · Accessibility, Internationalisation and Print
Tailwind gives you variants for the user's preferences and situation: focus-visible:, motion-reduce:, contrast-more:, forced-colors:, rtl:, print:.
The variants are easy to write and easy to get wrong without noticing, because a page that ignores these needs still looks fine to its author. So this chapter does what the earlier ones did: it turns each preference on in a real
browser and measures what happens. The course Web Accessibility (a11y) covers the standards and the reasoning in depth; this chapter is about the Tailwind side.
Emulation.setEmulatedMedia for the preferences; Accessibility.getFullAXTree
for what assistive technology is offered; Page.printToPDF for print). Not run: a real screen reader, a real Windows contrast theme (Chrome's emulated forced colours only), a real printer, an automated
audit tool such as axe, and any browser except Chrome. Passing these checks does not make a page accessible; they show what each class does.
Focus Styles
Keyboard users need to see where they are. Three habits matter, and I tested each on four controls: a button using focus:, a button using focus-visible:, a button with outline-hidden and
no replacement, and a text input using focus-visible:. I clicked each with the mouse, then pressed Tab from a fresh page load.
The pattern to adopt: use focus-visible: for the ring (mouse users on buttons are not shown it, keyboard users always are), and never use outline-hidden or outline-none without putting a visible
replacement on focus-visible:. The third button is the classic bug: it looks tidier and has no focus indicator at all. Chapter 8 explained that outline-hidden keeps a transparent outline in forced-colours
mode; it still shows nothing in normal mode.
Text for Screen Readers Only
A star icon on a link has no text. You can add text that is invisible on screen but still read aloud. But hidden and invisible remove text from assistive technology as well. I asked Chrome for the
accessibility tree and read the name it computed for each of five links:
Only sr-only keeps the words in the name. The same trick builds a skip link, a link that is invisible until a keyboard user reaches it:
focus:not-sr-only undoes the hiding exactly when the link receives focus, which is why the skip link is both hidden for mouse users and reachable by keyboard.
Contrast
The WCAG minimum for normal text is 4.5 to 1 (3 to 1 for large text and interface parts). Tailwind's palette is not guaranteed to meet it, and the common mistakes are light greys and white text on mid-tone colours. I measured the ratio for common pairs by reading each element's computed colours, converting them to sRGB through a canvas, and applying the WCAG formula:
| Text | Background | Ratio | Result |
|---|---|---|---|
text-slate-400 (#90a1b9) | white | 2.63 | fails |
text-gray-400 (#99a1af) | white | 2.60 | fails |
text-slate-500 (#62748e) | white | 4.76 | passes |
text-gray-500 (#6a7282) | white | 4.84 | passes |
text-slate-500 | bg-slate-100 (#f1f5f9) | 4.35 | fails for normal text |
text-slate-600 (#45556c) | white | 7.58 | passes |
| white | bg-blue-500 (#2b7fff) | 3.76 | large text only |
| white | bg-blue-600 (#155dfc) | 5.25 | passes |
| white | bg-red-500 (#fb2c36) | 3.81 | large text only |
| white | bg-emerald-500 (#00bc7d) | 2.47 | fails |
| white | bg-emerald-700 (#007a55) | 5.36 | passes |
text-slate-900 | bg-yellow-300 (#ffdf20) | 13.44 | passes |
Two surprises. A grey that passes on white (slate-500, 4.76) fails on a light grey panel (4.35). And white on emerald-500 is only 2.47, worse than most people guess; one step darker passes.
Users can also ask for more or less contrast, and Tailwind has variants for that. With contrast-more: and contrast-less: set on a paragraph and a border, I switched Chrome's emulated
prefers-contrast:
Reduced Motion
Some people feel ill from animation. The setting is prefers-reduced-motion; Tailwind gives two variants, motion-reduce: (the user asked for less) and motion-safe: (they did not). I used four
elements and switched the emulated preference:
The lesson is in the first row: nothing is automatic. An animation without one of the two variants runs for everybody. The safer default is motion-safe: on the animation itself, so that it exists only when the user has not
asked for less.
Forced Colours
In Windows High Contrast and similar modes the browser replaces your colours with the user's chosen system palette. The variant is forced-colors:, with not-forced-colors: for the opposite and
forced-color-adjust-none to opt an element out. I switched Chrome's emulated forced-colors: active:
The browser already handled x2, so you rarely need to write forced-colors: yourself. The value of forced-color-adjust-none is also its danger: x3 kept your colours, so you now own the contrast.
The cases that need a variant are decorative separators and custom controls built from background colours alone: in forced colours a background-only bar can vanish, so give it a border or an outline. I tested with Chrome's
emulation, not a real Windows contrast theme.
Logical Properties and Right-to-Left
ml-4 always means "left". For a right-to-left language (Arabic, Hebrew) you want "start". Tailwind's logical utilities (ms-, me-, ps-, pe-, border-s,
start-, end-, text-start) follow the writing direction. I laid out one block with logical classes and one with physical ones, then set dir="rtl" on their container:
The logical block mirrored by itself; the physical one stayed put. rtl: and ltr: are for the cases logical utilities cannot express (an icon that must flip, or a different colour).
Tailwind 4.3.3 also has logical block-axis utilities. I built each and checked the generated declaration:
| Class | Generates | Exists in 4.3.3 |
|---|---|---|
pbs-4, pbe-1 | padding-block-start, padding-block-end | yes |
mbs-3, mbe-2 | margin-block-start, margin-block-end | yes |
inline-4, block-8 | inline-size, block-size | yes |
min-inline-2, max-block-10 | min-inline-size, max-block-size | yes |
border-bs-2 | border-block-start-width | yes |
inset-s-1, inset-bs-0, start-0 | inset-inline-start, inset-block-start | yes |
pis-2, pie-3, mis-1, mie-4 | nothing | no: use ps-, pe-, ms-, me- |
Note the last row: the CSS property names (padding-inline-start) suggest pis-, but Tailwind's inline-axis names are the shorter ps- and pe-, and the block-axis ones are pbs- and pbe-.
I did not check which Tailwind release added the block-axis utilities; they exist in 4.3.3.
Print Styles
The print: variant applies under @media print. I emulated print media for the computed styles, and used Chrome's PDF output (A4, 0.4 inch margins) to see real page breaks:
The PDF test used a navigation bar (160 px tall) and three sections of 320 px:
Hiding the navigation brought the document from two pages to one; break-before-page forces a section onto its own page. break-inside-avoid is the companion that keeps a card from splitting across two pages.
One thing I assumed and got wrong: Tailwind 4.3.3 has no print-color-adjust utility (print-color-adjust-exact produced no rule). The arbitrary property [print-color-adjust:exact] does work, and
does generate print-color-adjust: exact. Whether backgrounds print also depends on the user's print dialog; my PDF test asked for them explicitly (printBackground). The site's own PDF builder works by loading
chapters in headless Chrome, so print: rules in a chapter should apply to its PDF (I did not test that builder).
An Accessibility Review Checklist for Tailwind
- Every interactive element has a visible keyboard focus style on
focus-visible:; no bareoutline-hiddenoroutline-none. - Icon-only controls have a name:
sr-onlytext oraria-label; neverhiddenorinvisibletext. - A skip link exists (
sr-only focus:not-sr-only). - Text and interface colours pass the WCAG ratios (measure; do not guess:
emerald-500with white text fails);contrast-more:variants for subtle greys. - Every animation is under
motion-safe:or hasmotion-reduce:alternatives. - No meaning conveyed by background colour alone (forced colours removes it);
forced-color-adjust-noneonly where you have checked contrast. - Layout uses logical utilities (
ms-,ps-,start-,text-start);rtl:only for things that must flip. - Print: hide navigation, show URLs or notes if needed, set break behaviour, and test the PDF.
2. I counted four Tab presses to reach the skip link and landed on the wrong control. The page has four earlier focusable elements. How to avoid it: print the focused element's id after each key press instead of counting.
3. I assumed a
print-color-adjust-exact utility. It produced no rule. How to avoid it: build the class and look for a rule before writing it into a lesson.4. My first print test fitted on one page whether or not the navigation was hidden, so it proved nothing. I made the sections taller. How to avoid it: work out what result would show a difference before running the test.
Hands-On Exercises
Make four controls (focus:, focus-visible:, outline-hidden alone, an input). Click each with the mouse and Tab through them, and report :focus-visible and the outline for each case. Then build five links (sr-only, hidden, invisible, aria-label, a skip link) and read their names from the accessibility tree.
Measure the WCAG contrast ratio of twelve Tailwind colour pairs by reading computed colours. Include a result you can predict as a check. Then show what contrast-more:, motion-reduce:, motion-safe: and forced-colors: change by switching the emulated media features.
Build a block with logical classes and one with physical classes, switch the container to dir="rtl" and report what moved. Then make a page that prints to one PDF page with print:hidden on the navigation and to three pages with break-before-page, and report the page counts.
Chapter 9 Quick Reference
- Focus ring on
focus-visible:: mouse clicks on buttons do not show it, keyboard always does.outline-hiddenwith no replacement left keyboard focus invisible sr-onlykeeps text in the accessible name (the accessibility tree showed it);hiddenandinvisibleremove it. Skip link:sr-only focus:not-sr-only focus:absolute focus:left-2 focus:top-2 focus:bg-white- Contrast is not guaranteed by the palette: slate-400/white 2.63, white on emerald-500 2.47 fail; slate-500/white 4.76 and white on blue-600 5.25 pass.
contrast-more:/contrast-less:respond toprefers-contrast - Animations run for everyone unless you add
motion-safe:ormotion-reduce:;motion-reduce:transition-nonesets the property to none forced-colors:/not-forced-colors:/forced-color-adjust-none: system colours replace yours; backgrounds alone can vanish; opting out makes contrast your job (tested with emulation only)- Logical utilities mirror in RTL by themselves (
ms-me-ps-pe-border-sstart-end-text-start); physicalml-does not.pbs-/pbe-/mbs-/mbe-/inline-/block-exist in 4.3.3;pis-/mis-do not print:hiddentook a test document from 2 PDF pages to 1;break-before-pagegave 3; noprint-color-adjust-*utility (use[print-color-adjust:exact])
cva variants and a documentation page, wired into Vite and
into a second tool, with a shipping checklist and the course recap. Start with Tailwind CSS Intermediate/Advanced 10.