opsinjs
ComponentsUtility

VisuallyHidden

Content that screen readers announce and eyes do not see.

Preview

Theme
Density
Text
Status
visually-hidden · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/visually-hidden

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { VisuallyHidden } from "@/components/ui/visually-hidden"
<button type="button">
  <XIcon aria-hidden="true" />
  <VisuallyHidden>Close the panel</VisuallyHidden>
</button>

When to use it

Use it when

  • Give an icon-only control an accessible name, by putting the words the icon stands for inside it.
  • Add context a sighted reader gets from layout, such as the subject of a repeated "Read more" link.
  • Provide a heading or a label for a region that reads clearly to the eye without one.

Do not use it when

  • The content must become visible when it receives focus, which is how a skip link works.Use link instead.
  • You are labelling a form input and want the label, the hint and the error wired together.Use field instead.
  • The words explain jargon and should be readable by everyone, not only by assistive technology.Use term instead.

Examples

A name for an icon-only button

The button shows only a cross, so a screen-reader user meets a control with no name. The words inside VisuallyHidden become its accessible name, and the icon is aria-hidden.

Theme
Density
Text
Status
visually-hidden-a-name-for-an-icon-button · base base · style base-lyraOpen under the product theme

Three rows end in the same two words, which a screen reader can list with no sentence around them. Appending the subject inside VisuallyHidden names each link without changing a pixel.

Theme
Density
Text
Status
visually-hidden-context-for-a-repeated-link · base base · style base-lyraOpen under the product theme

Content guidelines

Write the hidden words a sighted reader already has, no more. For an icon-only control, name the action in full. Keep the leading space when you splice hidden words into visible text, because a screen reader concatenates them. Never hide a caveat, a source or a clinical status, because everyone needs those.

Do

"Close the reminders panel" names the action a sighted reader takes from the cross.

Don’t

An empty VisuallyHidden names nothing, and the markup now looks labelled when it is not.

Accessibility

Checked by pnpm run check:a11y on every commit. The component sets no colour and no type size, so the gate's token and unit rules pass trivially.

Audited against WCAG 2.2 AA in a source pass and a rendered pass. The audit is author run, not an independent review, and clinical review is still pending. The source pass covered the clip technique, the accessibility tree and the two shipped examples; the rendered pass ran axe against the live preview and checked target size, focus visibility and reflow at 320px, and found nothing to flag.

  • Content is announced because the span is clipped, not display:none, visibility:hidden or the hidden attribute, each of which removes content from the tree. The clip pattern was checked against the WebAIM sr-only reference and stays hidden through 200% text and increased text spacing, because neither touches the clip's own overflow and clip-path.
  • It is text rather than a control, so it is never focusable and never a tab stop.
  • The audit fixed one gap the component's own open question named: nothing previously warned when a focusable control was placed inside the always hidden clip, a keyboard trap with no visible focus ring. The component now walks its children in development and warns when it finds a focusable descendant, the same pattern it already used for empty children. The warning is development only, so it changes no markup, class or visual output.
  • The component paints nothing, so no colour or type size applies and there is nothing to reflow at 200% text.

Not checked by any gate. Whether a screen reader announces the content across VoiceOver, NVDA, JAWS and TalkBack. Whether hidden text duplicates a visible name beside it is a per-usage question the component cannot see from its own source, so it stays a concern for the caller rather than something this audit could check.

KeyActionNotes
TabSkips the hidden textNever a tab stop. A control placed inside keeps its own place in the order.
Shift+TabSkips the hidden text in reverseReverse order matches forward order, because there is nothing to stop on.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

API reference

Prop

Type

Generated from VisuallyHiddenProps in registry/bases/base/visually-hidden.tsx.

children is required, because a VisuallyHidden with nothing to announce is a silent no-op. Empty children, an empty string, undefined or false raise a development warning naming the mistake. The span still renders, because a presentation layer reports rather than throws. className reaches the span for positioning, and a class that makes the content visible defeats the component.

  • Link is the anchor a skip link is built from, and a skip link becomes visible on focus.
  • Field names a visible input and wires its label, its hint and its error together.
  • Term renders a definition every reader can see, not one for assistive technology alone.

On this page