Skip to content

Visually hidden

Sometimes a screen reader needs words that sighted users get from the layout or an icon. Two classes in @arun-dev/ui/components.css keep text in the page for assistive technology without drawing it. They’re classes, not a component, because they need nothing but CSS.

ClassHiddenUse it for
sr-onlyAlwaysA label, or extra context, for screen readers
sr-only-focusableUntil it, or something inside it, has focusA skip link
Release notes ↗ (opens in a new tab)3 unread messages
import { Badge, Button, Link, Stack } from '@arun-dev/ui';
export default function VisuallyHiddenBasics() {
return (
<Stack gap="sm" align="start">
{/* Extra context only a screen reader needs: sighted users see the arrow. */}
<Link href="https://example.com" target="_blank" rel="noreferrer">
Release notes ↗<span className="sr-only"> (opens in a new tab)</span>
</Link>
{/* A button with only an icon still needs a name. */}
<Button>
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1a5 5 0 0 0-5 5v3L1.5 11v1h13v-1L13 9V6a5 5 0 0 0-5-5Zm0 15a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2Z" />
</svg>
<span className="sr-only">Notifications</span>
</Button>
{/* A number whose meaning is clear from the layout, spelled out for a screen reader. */}
<Badge tone="info">
3<span className="sr-only"> unread messages</span>
</Badge>
</Stack>
);
}

The text stays in the page, so it’s read out, found by in-page search, and translated by the browser’s translate tool. It’s drawn as a 1px box clipped to nothing, rather than with display: none or visibility: hidden, which would hide it from screen readers too.

Hidden until it’s focused: a skip link at the top of the page lets keyboard users jump past the navigation. It appears when tabbed to, and hides again when focus moves on.

Skip to the demo content

Click just above this text, then press Tab: the skip link appears while it has focus.

The content the link skips to.

import { Link, Paragraph, Stack } from '@arun-dev/ui';
// Shown only while focused, so it can be styled as a visible pill when it appears.
const pill = {
alignSelf: 'start',
padding: 'var(--space-2xs) var(--space-sm)',
borderRadius: 'var(--radius-md)',
backgroundColor: 'var(--color-text-accent)',
color: 'var(--color-text-on-accent)',
textDecorationLine: 'none',
};
export default function VisuallyHiddenSkipLink() {
return (
<Stack gap="sm">
<Link className="sr-only-focusable" href="#skip-demo-content" style={pill}>
Skip to the demo content
</Link>
<Paragraph color="secondary">
Click just above this text, then press Tab: the skip link appears while it has focus.
</Paragraph>
<Paragraph id="skip-demo-content" tabIndex={-1}>
The content the link skips to.
</Paragraph>
</Stack>
);
}

It hides itself only while not focused, so style it for how it looks when it appears. Give the target tabIndex={-1} so focus moves there too, not only the scroll position.

GoalUse
Hidden from sight, read by screen readerssr-only
Hidden until focusedsr-only-focusable
Hidden from everyonethe hidden attribute, or display: none
Shown, but not read out (a decorative icon)aria-hidden="true"
A name for an element with no text of its ownaria-label, or sr-only text inside it
  • Prefer visible text when there’s room for it. Hidden text helps screen reader users, but not people who’d benefit from seeing the words.
  • Never put sr-only on something focusable, like a link or a button: keyboard users would tab to something they can’t see. Use sr-only-focusable.
  • Inside an element with an accessible name, such as a Button, sr-only text becomes part of that name. aria-label replaces the whole name; sr-only text adds to the visible text, so voice control users can still say what they see.
  • The classes are in the utilities layer, the last one, so they win over a component’s own styles on the same element.