Visually hidden
Text a screen reader reads but the screen doesn't show. It names icon buttons, code inputs and phone headers that are clear by sight alone.
Playground
Change the text to try it. Nothing shows in the preview: the text is there for screen readers only. The code under the preview updates as you go; copy it into your page.
import { VisuallyHidden } from '@/ui/visually-hidden';<VisuallyHidden> More actions for Maya Chen</VisuallyHidden>Hidden field label
A search field whose purpose is plain on screen still needs a real label. The label stays in the markup and leaves the screen. A placeholder is never the only name.
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
import { VisuallyHidden } from '@/ui/visually-hidden';
export default function VisuallyHiddenFieldLabel() {
return (
<div className="flex h-7 w-64 items-center gap-075 rounded-large bg-surface px-100 shadow-control">
<MagnifyingGlassIcon className="size-4 shrink-0 text-text-subtle" />
<label htmlFor="candidate-search">
<VisuallyHidden>Search candidates</VisuallyHidden>
</label>
<input id="candidate-search" type="search" placeholder="Search candidates" className="min-w-0 flex-1 bg-transparent text-body text-text outline-none placeholder:text-text-subtle" />
</div>
);
}Extra context
A number that sits next to its heading on screen needs its meaning spelled out for a screen reader, which reads it on its own. Here each count is read as "3 candidates at Interview".
- Interview3 candidates at Interview
- CV sent5 candidates at CV sent
- Offer1 candidates at Offer
import { VisuallyHidden } from '@/ui/visually-hidden';
export default function VisuallyHiddenExtraContext() {
return (
<ul className="flex w-64 flex-col gap-050 text-body text-text">
{[
['Interview', 3],
['CV sent', 5],
['Offer', 1],
].map(([stage, count]) => (
<li key={stage} className="flex items-center justify-between rounded-medium bg-surface px-100 py-050 shadow-control">
<span>{stage}</span>
<span className="text-text-subtle tabular-nums">
{count}
<VisuallyHidden> candidates at {stage}</VisuallyHidden>
</span>
</li>
))}
</ul>
);
}Icon button
An icon button doesn't need this component. Give it label, which becomes its accessible name and its tooltip.
import { DotsThreeIcon } from '@phosphor-icons/react';
import { IconButton } from '@/ui/button';
export default function VisuallyHiddenIconButton() {
return (
<IconButton label="More actions for Maya Chen">
<DotsThreeIcon />
</IconButton>
);
}Task row and task form
A Task is a row: what to do, who it is about and when it is due, with one way to complete it. A new Task is a draft row in the Tasks table, never a dialog.
Ask bar
The one floating way into Flo and search on every page. It opens a search that asks Flo when nothing matches, and carries the review count and the mic.