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-visibleshows 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
remunits 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, andCanvasTextautomatically 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
titleattribute (hover tooltip — not reliable for touch/keyboard). - Via
aria-labelcontaining 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.