Screen Readers
Some content should exist for assistive technology without ever appearing on screen: extra context for an icon-only button, a label that clarifies an ambiguous link, a skip-navigation link that only sighted keyboard users need to see. display: none and visibility: hidden don't work for this — both remove the element from the accessibility tree along with the visual one. .sr-only solves that by hiding an element visually while leaving it fully readable by screen readers.
.sr-only
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
Every line here is load-bearing:
position: absolutepulls the element out of normal flow, so it can't push other content around.width: 1px; height: 1pxshrinks it to a single pixel instead of0×0— some older assistive technologies ignore zero-sized elements entirely.margin: -1pxcancels out that 1px box so it doesn't reintroduce any visible spacing.overflow: hidden+clip: rect(0, 0, 0, 0)clips the content to nothing —clipis deprecated in favor ofclip-path, but is kept here for the wider legacy screen-reader support it still guarantees.white-space: nowrapstops long text from wrapping and inflating the 1px box back to something visible.border-width: 0strips any inherited border that could otherwise poke outside the 1px box.
<button class="btn">
<svg aria-hidden="true">...</svg>
<span class="sr-only">Delete item</span>
</button>
<a href="/pricing">
View plans <span class="sr-only">(opens pricing page)</span>
</a>
In both examples, sighted users see a clean icon or a short link label, while screen reader users hear the fuller, unambiguous version.
.sr-only-focusable
Sometimes hidden content needs to become visible the moment it's reached — the classic case is a "Skip to main content" link, which should stay invisible for mouse users but appear the instant a keyboard user tabs to it.
.sr-only-focusable:not(:focus):not(:focus-within) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
The visually-hidden styles are wrapped in :not(:focus):not(:focus-within). As soon as the element itself is focused, or a descendant inside it is, the selector stops matching — and every one of those properties simply falls away, letting the element render normally, in-flow, at full size.
<body>
<a href="#main-content" class="sr-only-focusable focus-ring">
Skip to main content
</a>
<nav>...</nav>
<main id="main-content">...</main>
</body>
Placed as the very first focusable element in <body>, this link is invisible on page load, appears the moment Tab reaches it, and jumps focus straight past the navigation when activated. Pairing it with Focus Ring gives it a visible, on-brand focus indicator once it appears.
.sr-only vs. aria-label
Both make text available to assistive technology without a visible label — the right choice depends on what else is on the element.
| Situation | Recommended approach |
|---|---|
| Icon-only button, no visible text at all | aria-label — simplest, no extra markup |
| Visible text exists, but needs extra context for AT | .sr-only — appends to the existing accessible name |
| Skip links, "jump to" navigation aids | .sr-only-focusable — needs to become visible on focus |
| Content should be identical on screen and for AT, just styled differently | .sr-only — avoids maintaining two separate strings |
As a rule of thumb: reach for aria-label first when there's no visible text to build on, and reach for .sr-only when you're supplementing text that's already there.