import { Icon } from '@vega-ui/react';
import {
Bell,
Check,
Heart,
Star,
} from '@vega-ui/icons';
<div>
<Icon><Check /></Icon>
<Icon><Heart /></Icon>
<Icon><Star /></Icon>- 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
- :where()
Summary
Icon is the shared SVG wrapper for VegaUI icons and custom SVG content. It normalizes size through tokens and renders through Slot, which lets the child SVG receive the merged props directly.
Imports
import { Icon, type IconProps, type IconSize } from '@vega-ui/react';Exported Types
IconPropsIconSize
Basic Usage
import { Icon } from '@vega-ui/react';
import { CalendarIcon, MessageSquareIcon, PaperclipIcon } from '@vega-ui/icons';
export default function BasicUsage() {
return (
<ul style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-4) var(--spacing-10)', margin: 0, padding: 0, listStyle: 'none', color: 'var(--label-secondary)' }}>
<li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
<Icon size='sm'><CalendarIcon /></Icon>
Due May 14
</li>
<li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
<Icon size='sm'><MessageSquareIcon /></Icon>Variants
- Size:
4xs,3xs,2xs,xs,sm,md,lg,xl - Rendering: token size or explicit
width/height - Content: VegaUI icon, custom SVG, or third-party SVG component
Edge Cases
- Explicit
widthorheightbypasses the token size data attribute. Iconsetsaria-hidden='true'by default, which is correct for decorative usage but not for standalone semantic graphics.stroke='currentColor'means icon color comes from surrounding text or control color.
Common Mistakes
- Using icon color alone to express state.
- Letting icon size drift away from the surrounding control density.
- Assuming every icon should be announced to assistive technology.
Examples
Basic
Basic: icons beside text
Icon adds aria-hidden='true' and stroke='currentColor' to its child SVG, so a glyph next to text is decorative and takes the text color of its parent.
import { Icon, Text } from '@vega-ui/react';
import { CircleCheckIcon, ShieldIcon, TriangleAlertIcon } from '@vega-ui/icons';
export default function InfoRow() {
return (
<ul style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-4)', margin: 0, padding: 0, listStyle: 'none', width: '100%' }}>
<li style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--spacing-4)' }}>
<Icon size='sm' style={{ flexShrink: 0, marginBlockStart: 'var(--spacing-3)', color: 'var(--color-green-accent-500)' }}><CircleCheckIcon /></Icon>
<Text size={3}>SSO is enabled for this workspace</Text>
</li>
<li style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--spacing-4)' }}>
<Icon size='sm' style={{ flexShrink: 0, marginBlockStart: 'var(--spacing-3)', color: 'var(--color-primary-500)' }}><ShieldIcon /></Icon>Basic: size scale
size maps to fixed pixel sizes from 4xs (8px) to xl (28px). Pick the size that matches the text or control next to the icon.
import { Icon, type IconSize } from '@vega-ui/react';
import { StarIcon } from '@vega-ui/icons';
const sizes: IconSize[] = ['4xs', '3xs', '2xs', 'xs', 'sm', 'md', 'lg', 'xl'];
export default function Sizes() {
return (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 'var(--spacing-8)' }}>
{sizes.map((size) => (
<figure key={size} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-3)', margin: 0 }}>
<Icon size={size}><StarIcon /></Icon>
<figcaption style={{ color: 'var(--label-secondary)', fontSize: 'var(--font-size-1)' }}>{size}</figcaption>Controlled/Stateful
Controlled/Stateful: disclosure chevron
The chevron rotates with CSS when the section opens; the state itself is exposed by aria-expanded on the button, never by the icon.
import { Button, Icon, Text } from '@vega-ui/react';
import { ChevronDownIcon } from '@vega-ui/icons';
import { useState } from 'react';
import './examples.css';
export default function DisclosureChevron() {
const [expanded, setExpanded] = useState(false);
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--spacing-4)', width: '100%' }}>
<Button
type='button'Form/Integration
Form/Integration: leading icon inside a text field
TextField is a flex container, so an Icon placed before TextFieldInput becomes an adornment. The field keeps its visible Label; the icon only decorates.
import { Icon, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { AtSignIcon } from '@vega-ui/icons';
import { FormEvent, useState } from 'react';
export default function InputAdornment() {
const [handle, setHandle] = useState('');
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setHandle(String(new FormData(event.currentTarget).get('handle')));
};
Layout/Overlay
Layout/Overlay: icons inside buttons
Leading and trailing icons sit in the button's flex gap. Inside Button the icon does not scale automatically, so set size to match the button size.
import { Button, Icon } from '@vega-ui/react';
import { ChevronDownIcon, DownloadIcon, PlusIcon } from '@vega-ui/icons';
export default function IconInsideAButton() {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-4)' }}>
<Button type='button'>
<Icon size='sm'><PlusIcon /></Icon>
Add member
</Button>
<Button type='button' variant='secondary' appearance='ghost'>
ExportError
Error: icon in a validation message
The field validates on blur. The error text, not the icon, carries the meaning; the icon inherits the error color from HelperText and the message is linked with aria-describedby.
import { HelperText, Icon, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { CircleAlertIcon } from '@vega-ui/icons';
import { useState } from 'react';
export default function ErrorMessageIcon() {
const [error, setError] = useState('');
const validate = (input: HTMLInputElement) => {
if (input.validity.valid) {
setError('');
return;
}Disabled
Disabled: icons in disabled controls
Because the glyph uses currentColor, it takes the disabled text color of its button without extra styles. The same works for muted helper copy.
import { Button, Icon, IconButton } from '@vega-ui/react';
import { LockIcon, Trash2Icon, UploadIcon } from '@vega-ui/icons';
export default function MutedControlIcon() {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--spacing-6)', width: '100%' }}>
<div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
<Button type='button' disabled>
<Icon size='sm'><UploadIcon /></Icon>
Upload logo
</Button>
<IconButton type='button' variant='secondary' appearance='ghost' aria-label='Delete logo' disabled>Edge
Edge: icon as the only status signal
In a status column there is no visible text, and the icon stays aria-hidden. A VisuallyHidden span gives screen readers the status in words.
import { Icon, VisuallyHidden } from '@vega-ui/react';
import { CircleCheckIcon, CircleXIcon } from '@vega-ui/icons';
const checks = [
{ name: 'Unit tests', passed: true },
{ name: 'Lint', passed: true },
{ name: 'Visual regression', passed: false },
];
export default function StandaloneMeaningfulIcon() {
return (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>Edge: directional icons in right-to-left layouts
Arrows point in the reading direction, so in dir='rtl' they must be mirrored. Flip only directional glyphs; icons like a check or a clock stay as they are.
import { Button, Icon } from '@vega-ui/react';
import { ArrowRightIcon } from '@vega-ui/icons';
export default function RtlDirectionalIcon() {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--spacing-4)' }}>
<div dir='ltr'>
<Button type='button'>
Continue
<Icon size='sm'><ArrowRightIcon /></Icon>
</Button>
</div>Edge: size outside the scale
For an illustration-sized icon, set --icon-size directly. The width and height props are not forwarded to the SVG at the moment, so do not rely on them.
import type { CSSProperties } from 'react';
import { Heading, Icon, Text } from '@vega-ui/react';
import { InboxIcon } from '@vega-ui/icons';
export default function CustomIconSize() {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-8)', width: '100%', textAlign: 'center' }}>
<Icon strokeWidth={1.25} style={{ '--icon-size': '64px', color: 'var(--label-tertiary)' } as CSSProperties}>
<InboxIcon />
</Icon>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-2)' }}>
<Heading as='h3' size={4} fontWeight={600}>Inbox zero</Heading>Anatomy
Overview
Icon is a single-part primitive that passes sizing and SVG props into its child through Slot.
Required Parts
Icon
Required. Owns the shared size token contract and SVG prop forwarding.
Optional Parts
There are no exported child parts.
Composition Order
Icon- one SVG child
Valid Composition Patterns
import { Icon } from '@vega-ui/react';
import { CalendarIcon, MessageSquareIcon, PaperclipIcon } from '@vega-ui/icons';
export default function BasicUsage() {
return (
<ul style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-4) var(--spacing-10)', margin: 0, padding: 0, listStyle: 'none', color: 'var(--label-secondary)' }}>
<li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
<Icon size='sm'><CalendarIcon /></Icon>
Due May 14
</li>
<li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
<Icon size='sm'><MessageSquareIcon /></Icon>Invalid Composition Patterns
Non-SVG child content
The wrapper is meant to forward SVG-like props and sizing.
Standalone meaningful icon with no accompanying label or context
The default aria-hidden behavior may make the meaning inaccessible.