Accessibility
EmilyCSS ships accessibility utilities as first-class output. Skip links, focus management, reduced motion, forced colours — generated from your config, not bolted on.
What ships by default
Every emilyCSS build includes a dedicated accessibility layer. You don't opt in — the classes are always there. This covers the four areas where teams consistently cut corners when building design systems.
sr-only / not-sr-only
Visually hides content while keeping it in the accessibility tree. Essential for skip links, icon button labels, and supplementary instructions.
focus-visible rings
Keyboard-only focus indicators using the focus-visible pseudo-class. Meets WCAG 2.2 SC 2.4.11 Focus Appearance when combined with sufficient contrast.
motion-reduce / motion-safe
Variants that respond to prefers-reduced-motion. Disable or replace transitions for users with vestibular or motion sensitivity.
forced-colors
Variant utilities for Windows High Contrast mode. Restore border/outline visibility on elements that rely on background colour for their state.
Skip links
A skip link lets keyboard users jump past the navigation directly to the main content. Without one, every page load requires tabbing through every nav item before reaching the content.
The pattern uses sr-only to visually hide the link, combined with focus:not-sr-only to make it visible when focused. The link appears at the top of the page the moment a keyboard user starts tabbing.
<!-- Place before your <nav> as the first focusable element --> <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-neutral-80 focus:text-neutral-10 focus:rounded"> Skip to main content </a> <!-- Target element --> <main id="main-content" tabindex="-1"> <!-- page content --> </main>
Place this as the first focusable element in your <body>, before the nav. The id="main-content" target should be on your <main> element.
Visually hidden: sr-only
sr-only hides content visually while keeping it in the accessibility tree. It uses a clip/position approach rather than display:none or visibility:hidden, which would remove it from both trees.
Use it for labels that make visual sense from context but need to be explicit for screen readers, and for supplementary instructions that only assistive technology needs.
<!-- Icon button: label hidden visually, announced by screen readers --> <button class="p-2"> <svg aria-hidden="true" .../> <span class="sr-only">Close menu</span> </button> <!-- Table caption visible to screen readers only --> <table> <caption class="sr-only">Monthly revenue by product line, 2025</caption> ... </table> <!-- Supplementary form hint --> <input type="password" aria-describedby="pw-hint" /> <p id="pw-hint" class="sr-only">Must be at least 8 characters including a number</p> <!-- Toggle visibility on focus --> <a href="#main" class="sr-only focus:not-sr-only">Skip to content</a>
not-sr-only reverses the effect — useful with focus states (as in the skip link pattern) or when you need to toggle visibility programmatically.
Focus management
focus-visible: targets elements that are focused via keyboard or equivalent — it does not apply when the element is clicked. This is the correct variant for focus rings on buttons and links. Using focus: instead applies styles on click too, which is typically unnecessary and visually noisy for mouse users.
<!-- Ring on keyboard focus only, not on click --> <button class="focus-visible:ring-2 focus-visible:ring-brand-80 focus-visible:ring-offset-2 rounded"> Save changes </button> <!-- Link focus with outline --> <a href="/docs" class="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-80 rounded-sm"> Documentation </a> <!-- Remove default outline when providing a custom indicator --> <input type="text" class="outline-none focus-visible:ring-2 focus-visible:ring-brand-80 border border-neutral-30 rounded px-3 py-2" />
WCAG 2.2 Success Criterion 2.4.11 (Focus Appearance, AA) requires that the focus indicator has a minimum area and sufficient contrast. The ring-2 + ring-offset-2 combination with a high-contrast colour reliably meets this.
Reduced motion
The prefers-reduced-motion media query reflects a system-level preference set by users with vestibular disorders, epilepsy, or motion sensitivity. Ignoring it can cause genuine physical harm.
EmilyCSS generates motion-reduce: and motion-safe: variants. The correct pattern is to define your transition first, then opt it out — rather than conditionally applying it:
<!-- Define transition normally, then disable for reduced-motion users --> <div class="transition-transform duration-300 motion-reduce:transition-none"> Slides in on load </div> <!-- Decorative animation — skip entirely if user prefers reduced motion --> <div class="motion-safe:animate-pulse bg-neutral-20 rounded h-4 w-32"> Loading skeleton </div> <!-- Fade transitions — reduce to instant for motion-sensitive users --> <nav class="transition-opacity duration-200 motion-reduce:duration-0"> ... </nav>
motion-safe: is the inverse — it only applies when the user has not requested reduced motion. Use this for purely decorative animations that have no functional equivalent.
Forced colours
Forced colours mode (Windows High Contrast) overrides your CSS colour values with system colours. This helps users with low vision but can break UI patterns that rely on background-only visual states.
EmilyCSS generates forced-colors: variants so you can restore border or outline visibility for interactive elements that lose their background distinction in this mode.
<!-- Button that loses background in forced-colors mode --> <!-- Add an explicit border so the boundary is still visible --> <button class="bg-brand-80 text-white rounded px-4 py-2 forced-colors:border forced-colors:border-current"> Submit </button> <!-- Focus ring may need reinforcing in high contrast mode --> <a href="/docs" class="focus-visible:ring-2 focus-visible:ring-brand-80 forced-colors:focus-visible:outline forced-colors:focus-visible:outline-2"> Read the docs </a>
Doctor integration
emily-css doctor runs a set of static checks against your build output and config. Accessibility checks include:
- Whether
sr-onlyis present in your output (skipped = likely removed by purge incorrectly) - Whether focus-visible utilities are present and usable
- Whether your token contrast ratios are calculable from the generated shades
emily-css doctor # Example output: # ✓ sr-only present in output # ✓ focus-visible utilities found # ✓ motion-reduce variants generated # ✗ Contrast warning: --colour-text-40 on --colour-bg-10 may fall below 4.5:1 # Run a manual contrast check against your token values
Doctor does not replace a real accessibility audit. It catches obvious config and build-time issues — it can't test interaction behaviour or dynamic content.
What EmilyCSS does and doesn't cover
EmilyCSS provides utilities that make accessible patterns straightforward to implement. It does not audit your markup or guarantee WCAG conformance — that depends on how you use the classes.
EmilyCSS handles
- Generating sr-only and not-sr-only classes
- Focus-visible ring utilities with configurable colours
- motion-reduce / motion-safe variant generation
- forced-colors variant output
- Token contrast values that can be verified externally
Your responsibility
- Semantic HTML (correct heading order, landmark regions)
- ARIA attributes where required (labels, roles, states)
- Keyboard interaction patterns (focus order, trapping where needed)
- Colour contrast verification with a real contrast tool
- Manual and assistive technology testing
Next step
Next: Doctor checks for build-time diagnostics.