Skip to content

VisuallyHidden

Hides its children visually while keeping them in the accessibility tree. This is the canonical sr-only / visually-hidden pattern used by Tailwind, Bootstrap, and Radix — same CSS, just packaged as a primitive so the rule lives in one place.

Reach for it to label icon-only buttons, surface extra context next to inputs, or build a skip-to-content link. Don’t use it to hide content from sighted users for layout reasons — that’s display: none. This component keeps content audible on purpose.

Live Preview

Import

import { VisuallyHidden } from 'entangle-ui';

Usage

<VisuallyHidden>Search the catalog</VisuallyHidden>

Basic Hidden Content

Children render into the DOM but are clipped to a single transparent pixel. Inspect the element to confirm the text is still there.

Basic

<Text>
This sentence has{' '}
<VisuallyHidden>extra context only screen readers hear</VisuallyHidden>{' '}
invisible content embedded in it.
</Text>

With a Visible Sibling

Pair a visible label with a hidden description for a fuller screen-reader experience without cluttering the layout.

With visible sibling

<label>
<span>Email</span>
<VisuallyHidden>
We will only use your email to send password reset links.
</VisuallyHidden>
<input type="email" />
</label>

Set focusable to make the wrapper appear when it (or a descendant) gains focus. This is the standard skip-link pattern — wrap an anchor inside and let :focus-within reveal it.

Focusable (skip-to-content)

<VisuallyHidden focusable>
<a href="#main-content">Skip to main content</a>
</VisuallyHidden>

Hiding a Label for an Icon-Only Button

Pair a visible icon with a hidden label so assistive tech can announce the action.

Icon-only button

<button>
<SearchIcon />
<VisuallyHidden>Search the catalog</VisuallyHidden>
</button>

For most cases, prefer aria-label="Search" on the button itself — that’s terser. Use VisuallyHidden when you need rich children (e.g. interpolated values, formatted markup) that aria-label cannot express.

Common Pitfalls

  • Don’t use it to hide things from everyone. VisuallyHidden keeps content audible. Use display: none or hidden if the content should not exist for any user.
  • Don’t put interactive content inside it without focusable. A button hidden with the static styles still receives focus but stays clipped — sighted keyboard users cannot tell where they are.
  • Don’t add aria-hidden="true". That removes the content from the accessibility tree, defeating the purpose.

Props

Prop Type Default Description
children * ReactNode Content rendered inside the hidden region.
as 'span' | 'div' | 'label' | 'p' 'span' Element to render. Use `div` for block content or `label`/`p` when the semantics fit.
focusable boolean false When true, becomes visible when the element (or a descendant) is focused. Used for skip links.
className string Additional CSS class names.
style CSSProperties Inline styles applied to the rendered element.
testId string Test identifier for automated testing.
ref Ref<HTMLSpanElement> Ref to the rendered element.

Accessibility

  • Children remain in the DOM and the accessibility tree — screen readers, browser find-in-page, and translation tools all still see them
  • Implements the canonical SR-only style (position: absolute, clip: rect(0,0,0,0), width/height: 1px) so the element occupies no visual space but is not removed from layout flow
  • focusable reveals the element under :focus and :focus-within, which is exactly what skip links need