Skip to content
v2.5.0
English
Русский
Documentation

VisuallyHidden

VisuallyHidden keeps content available to assistive technology while removing it from visual layout.
visually-hidden.tsx · 7 lines
import { Icon, IconButton, VisuallyHidden } from '@vega-ui/react';
import { Search } from '@vega-ui/icons';

<IconButton variant='secondary' appearance='ghost'>
  <Icon><Search /></Icon>
  <VisuallyHidden>Search</VisuallyHidden>
</IconButton>
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
7 tests

Summary

VisuallyHidden keeps content available to assistive technology while removing it from visual layout. It supports a native wrapper or asChild composition through Slot.

Imports

import { VisuallyHidden, type VisuallyHiddenProps } from '@vega-ui/react';

Exported Types

  • VisuallyHiddenProps

Basic Usage

basic-usage.tsx · 37 lines
import { Heading, Link, Text, VisuallyHidden } from '@vega-ui/react';

const plans = [
  { name: 'Starter', price: '$0', slug: 'starter' },
  { name: 'Team', price: '$12', slug: 'team' },
  { name: 'Enterprise', price: 'Custom', slug: 'enterprise' },
];

export default function BasicUsage() {
  return (
    <ul style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 'var(--spacing-4)', width: '100%', margin: 0, padding: 0, listStyle: 'none' }}>
      {plans.map((plan) => (

Variants

  • Rendering: default wrapper or asChild
  • Usage: hidden label, description, or status text

Edge Cases

  • Hidden text should stay synchronized with the visible UI state.
  • VisuallyHidden is not a substitute for visible labels by default.
  • asChild keeps the hidden styling but changes the underlying element.

Common Mistakes

  • Leaving hidden text stale when the visible state changes.
  • Using hidden text to paper over poor visible labeling.
  • Hiding content that should actually be visible for most users.

Examples

Basic

Three "Learn more" links sound identical in a links list. Hidden text completes each name ("Learn more about the Team plan") without changing the layout.

basic-usage.tsx · 37 lines
import { Heading, Link, Text, VisuallyHidden } from '@vega-ui/react';

const plans = [
  { name: 'Starter', price: '$0', slug: 'starter' },
  { name: 'Team', price: '$12', slug: 'team' },
  { name: 'Enterprise', price: 'Custom', slug: 'enterprise' },
];

export default function BasicUsage() {
  return (
    <ul style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 'var(--spacing-4)', width: '100%', margin: 0, padding: 0, listStyle: 'none' }}>
      {plans.map((plan) => (

Basic: icon-only button with a text name

Hidden text inside the button becomes its accessible name and can include data the visual badge shows only as a number. The badge itself is aria-hidden so the count is not read twice. asChild renders a <span>, because the default <div> is not valid inside a button.

icon-only-control-label.tsx · 38 lines
import { Icon, IconButton, VisuallyHidden } from '@vega-ui/react';
import { BellIcon } from '@vega-ui/icons';
import { useState } from 'react';

export default function IconOnlyControlLabel() {
  const [unread, setUnread] = useState(4);

  return (
    <IconButton type='button' variant='secondary' appearance='ghost' onClick={() => setUnread(0)} style={{ position: 'relative' }}>
      <Icon><BellIcon /></Icon>
      <VisuallyHidden asChild>
        <span>Notifications, {unread} unread</span>

Controlled/Stateful

Controlled/Stateful: live announcement for a visual counter

The cart counter changes silently for screen readers. A hidden role='status' region receives a full sentence on every change, and the visual counter stays aria-hidden.

hidden-status-text.tsx · 35 lines
import { Button, Icon, VisuallyHidden } from '@vega-ui/react';
import { ShoppingCartIcon } from '@vega-ui/icons';
import { useState } from 'react';

const products = ['Linen shirt', 'Canvas tote', 'Wool socks'];

export default function HiddenStatusText() {
  const [cart, setCart] = useState<string[]>([]);
  const [announcement, setAnnouncement] = useState('');

  const add = (product: string) => {
    const next = [...cart, product];

Form/Integration

Form/Integration: hidden description for a visual adornment

The $ prefix is visual only. A hidden description linked with aria-describedby tells screen reader users the currency; the value is read with FormData on submit. VisuallyHidden does not accept id, so the id goes on the asChild span.

hidden-helper-description.tsx · 38 lines
import { Button, Label, TextField, TextFieldInput, VisuallyHidden } from '@vega-ui/react';
import { FormEvent, useState } from 'react';

export default function HiddenHelperDescription() {
  const [budget, setBudget] = useState('');

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setBudget(String(new FormData(event.currentTarget).get('budget')));
  };

  return (

Layout/Overlay

Layout/Overlay: hidden dialog title

A lightbox has no visible heading, but the dialog still needs a name. A hidden <h2> is referenced with aria-labelledby, so the dialog is announced as "Floor plan, apartment 4B".

hidden-dialog-cue.tsx · 59 lines
import type { CSSProperties } from 'react';
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Icon,
  IconButton,
  VisuallyHidden,
} from '@vega-ui/react';
import { ImageIcon, XIcon } from '@vega-ui/icons';

Error

Error: spoken prefix for an error message

The error is shown with color and an icon. A hidden "Error:" prefix gives the same signal to screen reader users when they reach the message through aria-describedby.

hidden-validation-cue.tsx · 48 lines
import { Button, HelperText, Icon, Label, TextField, TextFieldInput, VisuallyHidden } from '@vega-ui/react';
import { CircleAlertIcon } from '@vega-ui/icons';
import { FormEvent, useState } from 'react';

export default function HiddenValidationCue() {
  const [error, setError] = useState('');
  const [saved, setSaved] = useState('');

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const domain = String(new FormData(event.currentTarget).get('domain'));

Disabled

Disabled: why an option is unavailable

Sold-out sizes are disabled and struck through. The strike-through is visual only, so hidden text adds ", sold out" to the button name, which screen readers still read in browse mode.

hidden-disabled-explanation.tsx · 41 lines
import { Button, VisuallyHidden } from '@vega-ui/react';
import { useState } from 'react';

const sizes = [
  { label: 'S', inStock: true },
  { label: 'M', inStock: true },
  { label: 'L', inStock: false },
  { label: 'XL', inStock: false },
];

export default function HiddenDisabledExplanation() {
  const [selected, setSelected] = useState('M');

Edge

Edge: text alternative for a symbol rating

Star characters are read as "black star" one by one. They are hidden with aria-hidden, and a hidden span inside the paragraph reads "Rated 3 out of 5". asChild keeps a <span> inside the <p>, where a <div> is not allowed.

slotted-hidden-element.tsx · 27 lines
import { Text, VisuallyHidden } from '@vega-ui/react';

const reviews = [
  { author: 'Maria K.', rating: 5, text: 'Arrived in two days, fits perfectly.' },
  { author: 'Tom B.', rating: 3, text: 'Good fabric, but the sleeves run long.' },
];

export default function SlottedHiddenElement() {
  return (
    <ul style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-8)', width: '100%', margin: 0, padding: 0, listStyle: 'none' }}>
      {reviews.map((review) => (
        <li key={review.author} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-1)' }}>

Edge: empty-looking table header

An actions column often has no visible header, yet an empty <th> leaves the cells without context. A hidden "Actions" header fixes that while the column stays visually empty.

hidden-table-header.tsx · 51 lines
import { Icon, IconButton, VisuallyHidden } from '@vega-ui/react';
import { PencilIcon } from '@vega-ui/icons';
import { useState } from 'react';

const members = [
  { name: 'Jane Cooper', role: 'Owner' },
  { name: 'Wade Warren', role: 'Editor' },
  { name: 'Esther Howard', role: 'Viewer' },
];

export default function HiddenTableHeader() {
  const [editing, setEditing] = useState('');

Anatomy

Overview

VisuallyHidden is a single-part primitive that applies the standard visually-hidden CSS pattern to its content.

Required Parts

VisuallyHidden

Required. Owns the hidden-but-accessible rendering behavior.

Optional Parts

There are no exported child parts.

Composition Order

  1. VisuallyHidden
  2. hidden content

Valid Composition Patterns

basic-usage.tsx · 37 lines
import { Heading, Link, Text, VisuallyHidden } from '@vega-ui/react';

const plans = [
  { name: 'Starter', price: '$0', slug: 'starter' },
  { name: 'Team', price: '$12', slug: 'team' },
  { name: 'Enterprise', price: 'Custom', slug: 'enterprise' },
];

export default function BasicUsage() {
  return (
    <ul style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 'var(--spacing-4)', width: '100%', margin: 0, padding: 0, listStyle: 'none' }}>
      {plans.map((plan) => (

Invalid Composition Patterns

Hidden text used instead of visible label by default

The UI becomes harder to use for sighted users.

Hidden content left unsynchronized with the visible state

Assistive technology users receive stale information.