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
- Oklab and OkLCh — comes from the design tokens
- clip-path
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
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.
VisuallyHiddenis not a substitute for visible labels by default.asChildkeeps 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
Basic: unique names for repeated links
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.
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.
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.
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.
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".
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.
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.
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.
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.
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
VisuallyHidden- hidden content
Valid Composition Patterns
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.