Writing accessible CSS

CSS can make or break accessibility. While it controls the visual layer, it directly impacts readability, navigation, focus management, and the experience for users with various disabilities.

Visually hidden content (screen reader only)

Sometimes you need text that is read by screen readers but not visible on screen. Never use display: none or visibility: hidden for this — both remove elements from the accessibility tree.

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Allow the element to be focusable when navigated to */
.sr-only-focusable:focus,
.sr-only-focusable:active {
    position: static;
    width: auto;
    height: auto;
    padding: inherit;
    margin: inherit;
    overflow: visible;
    clip: auto;
    clip-path: none;
    white-space: normal;
}
<button>
    <svg aria-hidden="true"><!-- icon --></svg>
    <span class="sr-only">Close dialog</span>
</button>

Focus styles

Focus indicators are critical for keyboard navigation. Never remove them without providing an alternative.

/* Do not */
*:focus {
    outline: none;
}

/* Do — use :focus-visible for mouse-friendly, keyboard-accessible focus */
:focus-visible {
    outline: 3px solid #005fcc;
    outline-offset: 3px;
    border-radius: 2px;
}

/* Optional — remove focus ring for mouse clicks */
:focus:not(:focus-visible) {
    outline: none;
}

:focus-visible shows the focus ring only when the user navigates via keyboard, not mouse clicks. This is the modern best practice.

Responsive text and readability

Minimum text size

  • Body text should be at least 16px (1rem). Smaller text causes readability issues for low-vision users.
  • Use rem units so text scales with the user’s browser font size settings.
html {
    font-size: 100%; /* Respects user's browser settings */
}

body {
    font-size: 1rem;    /* 16px base */
    line-height: 1.6;   /* Comfortable reading */
}

small {
    font-size: 0.875rem; /* 14px — minimum for readable text */
}

Line length and spacing

  • Optimal line length is 45-75 characters per line. Wider text is hard to track.
  • Line height of at least 1.5 for body text (WCAG 1.4.12).
  • Paragraph spacing of at least 1.5× the font size.
.content {
    max-width: 70ch; /* roughly 70 characters wide */
    line-height: 1.6;
}

.content p + p {
    margin-top: 1.5em;
}

Respecting user preferences

Reduced motion

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

High contrast mode

Windows High Contrast Mode overrides most CSS colors. Use transparent outlines as fallbacks since they become visible in high contrast :

button {
    border: 2px solid transparent; /* Visible in High Contrast Mode */
    background: #005fcc;
    color: #fff;
}

button:focus-visible {
    outline: 3px solid transparent; /* Visible in High Contrast Mode */
    box-shadow: 0 0 0 3px #005fcc;
}

Forced colors

The forced-colors media query detects when the user has enabled a high contrast theme :

@media (forced-colors: active) {
    .custom-checkbox {
        border: 2px solid ButtonText;
    }

    .custom-checkbox.checked::after {
        background: Highlight;
    }
}

System color keywords like ButtonText, Highlight, Canvas, and CanvasText automatically map to the user’s chosen high contrast colors.

Scenarios and Edge Cases

Content reordering with CSS

Using order, flex-direction: row-reverse, or CSS Grid placement can create a disconnect between visual order and DOM order. Screen readers and keyboard navigation follow the DOM, not the visual layout :

/* Visual order: C, B, A — but keyboard tab order is still A, B, A */
.container {
    display: flex;
    flex-direction: row-reverse;
}

Rule : Visual order and DOM order must match. If you need to reorder visually, reorder in the HTML instead.

content property for meaningful text

The CSS content property in ::before and ::after is not consistently announced by screen readers. Never use it for meaningful content :

/* Do not — some screen readers skip this */
.required::after {
    content: ' (required)';
}

/* Do — put meaningful text in the HTML */
<label for="name">
    Name <span class="sr-only">(required)</span>
</label>

text-overflow: ellipsis and truncated content

Truncated text hides information from everyone. Ensure the full text is available :

  • Via a title attribute (hover tooltip — not reliable for touch/keyboard).
  • Via aria-label containing the full text.
  • Via an expandable mechanism.
<p class="truncated" title="Full text of this very long paragraph...">
    Full text of this very lo...
</p>

Hiding decorative content from screen readers

Use aria-hidden="true" for purely decorative elements that would add noise :

<span aria-hidden="true">🎉</span>
<span aria-hidden="true">|</span>
<span aria-hidden="true"></span>

Resources

If you found this helpful, share it with someone who's building for the web.

- Vinay Ranjan