---
title: "VisuallyHidden"
description: "Content that screen readers announce and eyes do not see."
url: "https://opsinjs.pensievelabs.org/components/visually-hidden"
source: "https://opsinjs.pensievelabs.org/components/visually-hidden.md"
section: "Components"
status: "shipped"
kind: "component"
category: "utility"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["sr only", "screen reader only", "clip"]
usedIn: ["diabetes-medicines-app"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="visually-hidden"
  status="shipped"
  questions="[
  &#x22;Nobody has listened to this component in VoiceOver, NVDA, JAWS or TalkBack, so announcement everywhere is argued from the clip pattern rather than measured.&#x22;,
  &#x22;Whether the component should refuse or warn when a focusable control sits inside it is unsettled, so an always-hidden keyboard trap is not prevented.&#x22;,
  &#x22;The contrast report is empty because nothing is painted, so a reviewer must read it as nothing to measure rather than a gap left unfilled.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="visually-hidden" />

## Installation [#installation]

<ComponentInstall name="visually-hidden" unbuilt="false" importPath="@/components/ui/visually-hidden" />

## Usage [#usage]

```tsx
import { VisuallyHidden } from "@/components/ui/visually-hidden"
```

```tsx
<button type="button">
  <XIcon aria-hidden="true" />
  <VisuallyHidden>Close the panel</VisuallyHidden>
</button>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;Give an icon-only control an accessible name, by putting the words the icon stands for inside it.&#x22;,
  &#x22;Add context a sighted reader gets from layout, such as the subject of a repeated \&#x22;Read more\&#x22; link.&#x22;,
  &#x22;Provide a heading or a label for a region that reads clearly to the eye without one.&#x22;,
]"
  avoid="[
  { case: &#x22;The content must become visible when it receives focus, which is how a skip link works.&#x22;, instead: &#x22;link&#x22; },
  { case: &#x22;You are labelling a form input and want the label, the hint and the error wired together.&#x22;, instead: &#x22;field&#x22; },
  { case: &#x22;The words explain jargon and should be readable by everyone, not only by assistive technology.&#x22;, instead: &#x22;term&#x22; },
]"
/>

## Examples [#examples]

### A name for an icon-only button [#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`.

<ComponentPreview name="visually-hidden-a-name-for-an-icon-button" kind="example" align="start" />

### Context for a repeated link [#context-for-a-repeated-link]

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.

<ComponentPreview name="visually-hidden-context-for-a-repeated-link" kind="example" align="start" />

## Content guidelines [#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.

<DoDont>
  <DoDont.Do>
    **"Close the reminders panel"** names the action a sighted reader takes from the cross.
  </DoDont.Do>

  <DoDont.Dont>
    **An empty VisuallyHidden** names nothing, and the markup now looks labelled when it is not.
  </DoDont.Dont>
</DoDont>

## Accessibility [#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.

<KeyboardTable
  name="visually-hidden"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Skips the hidden text&#x22;,
    notes: &#x22;Never a tab stop. A control placed inside keeps its own place in the order.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Skips the hidden text in reverse&#x22;,
    notes: &#x22;Reverse order matches forward order, because there is nothing to stop on.&#x22;,
  },
]"
/>

<ContrastReport component="visually-hidden" />

## API reference [#api-reference]

<PropsTable name="VisuallyHiddenProps" />

`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.

## Related [#related]

* [Link](./link.mdx) is the anchor a skip link is built from, and a skip link becomes visible on focus.
* [Field](./field.mdx) names a visible input and wires its label, its hint and its error together.
* [Term](./term.mdx) renders a definition every reader can see, not one for assistive technology alone.
