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.
| Class | Hidden | Use it for |
|---|---|---|
sr-only | Always | A label, or extra context, for screen readers |
sr-only-focusable | Until it, or something inside it, has focus | A skip link |
sr-only
Section titled “sr-only”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.
sr-only-focusable
Section titled “sr-only-focusable”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.
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.
Choosing how to hide
Section titled “Choosing how to hide”| Goal | Use |
|---|---|
| Hidden from sight, read by screen readers | sr-only |
| Hidden until focused | sr-only-focusable |
| Hidden from everyone | the 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 own | aria-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-onlyon something focusable, like a link or a button: keyboard users would tab to something they can’t see. Usesr-only-focusable. - Inside an element with an accessible name, such as a Button,
sr-onlytext becomes part of that name.aria-labelreplaces the whole name;sr-onlytext adds to the visible text, so voice control users can still say what they see. - The classes are in the
utilitieslayer, the last one, so they win over a component’s own styles on the same element.