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>
<Icon><Bell /></Icon>
</div>- Поддержка браузеров
- Широко доступно
- Chrome 111
- Edge 111
- Firefox 113
- Safari 15.4
- Safari на iOS 15.4
- Размер в бандле
- Протестирован в
- 6 тестов · 6 нояб.
- История версий
- Добавлен в v1.0.0, последнее изменение в v2.0.0
- Oklab and OkLCh — приходит из дизайн-токенов
- :where()
Кратко
Icon — общая SVG-обёртка для иконок VegaUI и собственного SVG-содержимого. Она нормализует размер через токены и рендерит через Slot, поэтому дочерний SVG напрямую получает объединённые пропсы.
Импорт
import { Icon, type IconProps, type IconSize } from '@vega-ui/react';Экспортируемые типы
IconPropsIconSize
Базовое использование
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>
12 comments
</li>
<li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
<Icon size='sm'><PaperclipIcon /></Icon>
3 files
</li>
</ul>
);
}Варианты
- Размер:
4xs,3xs,2xs,xs,sm,md,lg,xl - Способ задать размер: токен или явные
width/height - Содержимое: иконка VegaUI, собственный SVG или SVG-компонент сторонней библиотеки
Пограничные случаи
- Явные
widthилиheightобходят data-атрибут размера из токена. Iconпо умолчанию ставитaria-hidden='true'— это верно для декоративных иконок, но не для самостоятельной смысловой графики.stroke='currentColor'означает, что цвет иконки берётся из окружающего текста или элемента управления.
Частые ошибки
- Передача состояния только цветом иконки.
- Размер иконки расходится с плотностью окружающего элемента управления.
- Предположение, что каждую иконку должны объявлять вспомогательные технологии.
Примеры
Базовый
Базовый: иконки рядом с текстом
Icon добавляет дочернему SVG aria-hidden='true' и stroke='currentColor', поэтому глиф рядом с текстом декоративен и берёт цвет текста родителя.
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>
<Text size={3}>Two-factor authentication is required for admins</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-error)' }}><TriangleAlertIcon /></Icon>
<Text size={3}>2 members have not verified their email</Text>
</li>
</ul>
);
}Базовый: шкала размеров
size соответствует фиксированным размерам от 4xs (8px) до xl (28px). Выбирайте размер под текст или контрол рядом с иконкой.
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>
</figure>
))}
</div>
);
}Контролируемый/с состоянием
Контролируемый/с состоянием: шеврон раскрытия
Шеврон поворачивается через CSS, когда раздел раскрывается; само состояние сообщает aria-expanded на кнопке, а не иконка.
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'
variant='secondary'
appearance='ghost'
aria-expanded={expanded}
aria-controls='shipping-details'
onClick={() => setExpanded(!expanded)}
>
Shipping details
<Icon size='sm' className='disclosureChevron' style={{ transform: expanded ? 'rotate(180deg)' : 'none' }}>
<ChevronDownIcon />
</Icon>
</Button>
<div id='shipping-details' hidden={!expanded}>
<Text size={3} style={{ color: 'var(--label-secondary)' }}>Ships in 2–3 business days from Berlin. Free returns within 30 days.</Text>
</div>
</div>
);
}Форма/интеграция
Форма/интеграция: иконка в начале текстового поля
TextField — flex-контейнер, поэтому Icon перед TextFieldInput становится украшением поля. У поля остаётся видимый Label; иконка только декорирует.
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')));
};
return (
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%' }}>
<Label htmlFor='profile-handle'>Username</Label>
<TextField style={{ paddingInlineStart: 'var(--spacing-8)', gap: 'var(--spacing-3)' }}>
<Icon size='sm' style={{ flexShrink: 0, color: 'var(--label-secondary)' }}><AtSignIcon /></Icon>
<TextFieldInput id='profile-handle' name='handle' placeholder='jane.doe' pattern='[a-z0-9._]+' required style={{ paddingInlineStart: 0 }} />
</TextField>
<output style={{ color: 'var(--label-secondary)', overflowWrap: 'anywhere' }}>
{handle === '' ? 'Press Enter to save' : `Profile URL: example.com/@${handle}`}
</output>
</form>
);
}Раскладка/оверлей
Раскладка/оверлей: иконки внутри кнопок
Иконки в начале и в конце встают в flex-зазор кнопки. Внутри 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'>
Export
<Icon size='sm'><ChevronDownIcon /></Icon>
</Button>
<Button type='button' size='sm' variant='secondary' appearance='ghost'>
<Icon size='xs'><DownloadIcon /></Icon>
Invoice.pdf
</Button>
</div>
);
}Ошибка
Ошибка: иконка в сообщении валидации
Поле проверяется при потере фокуса. Смысл несёт текст ошибки, а не иконка; иконка наследует цвет ошибки от HelperText, а сообщение связано через 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;
}
setError('Enter an email like name@company.com');
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%' }}>
<Label htmlFor='billing-email'>Billing email</Label>
<TextField error={error !== ''}>
<TextFieldInput
id='billing-email'
type='email'
defaultValue='finance.acme.com'
required
aria-invalid={error !== ''}
aria-describedby={error === '' ? undefined : 'billing-email-error'}
onBlur={(event) => validate(event.currentTarget)}
/>
</TextField>
{error !== '' && (
<HelperText id='billing-email-error' error style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
<Icon size='xs' style={{ flexShrink: 0 }}><CircleAlertIcon /></Icon>
{error}
</HelperText>
)}
</div>
);
}Отключённый
Отключённый: иконки в отключённых контролах
Глиф использует currentColor, поэтому берёт цвет отключённого текста кнопки без дополнительных стилей. То же работает для приглушённого поясняющего текста.
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>
<Icon><Trash2Icon /></Icon>
</IconButton>
</div>
<p style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)', margin: 0, color: 'var(--disable-text-color)', fontSize: 'var(--font-size-2)' }}>
<Icon size='xs' style={{ flexShrink: 0 }}><LockIcon /></Icon>
Branding is managed by your organization admin
</p>
</div>
);
}Пограничный случай
Пограничный случай: иконка как единственный сигнал статуса
В колонке статуса нет видимого текста, а иконка остаётся aria-hidden. VisuallyHidden сообщает статус скринридеру словами.
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' }}>
<thead>
<tr>
<th scope='col' style={{ paddingBlock: 'var(--spacing-4)', borderBlockEnd: '1px solid var(--separator-opaque)', textAlign: 'start' }}>Check</th>
<th scope='col' style={{ paddingBlock: 'var(--spacing-4)', borderBlockEnd: '1px solid var(--separator-opaque)', textAlign: 'end' }}>Status</th>
</tr>
</thead>
<tbody>
{checks.map((check) => (
<tr key={check.name}>
<td style={{ paddingBlock: 'var(--spacing-4)', borderBlockEnd: '1px solid var(--separator-opaque)' }}>{check.name}</td>
<td style={{ paddingBlock: 'var(--spacing-4)', borderBlockEnd: '1px solid var(--separator-opaque)', textAlign: 'end' }}>
<Icon
size='sm'
style={{ color: check.passed ? 'var(--color-green-accent-500)' : 'var(--color-error)' }}
>
{check.passed ? <CircleCheckIcon /> : <CircleXIcon />}
</Icon>
<VisuallyHidden asChild>
<span>{check.passed ? 'Passed' : 'Failed'}</span>
</VisuallyHidden>
</td>
</tr>
))}
</tbody>
</table>
);
}Пограничный случай: направленные иконки в раскладке справа налево
Стрелки указывают в сторону чтения, поэтому в dir='rtl' их нужно отражать. Отражайте только направленные глифы; галочка или часы остаются как есть.
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>
<div dir='rtl' lang='ar'>
<Button type='button'>
متابعة
<Icon size='sm' style={{ transform: 'scaleX(-1)' }}><ArrowRightIcon /></Icon>
</Button>
</div>
</div>
);
}Пограничный случай: размер вне шкалы
Для иконки размером с иллюстрацию задайте --icon-size напрямую. Пропы width и height сейчас не передаются в SVG, поэтому не полагайтесь на них.
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>
<Text size={3} style={{ color: 'var(--label-secondary)', textWrap: 'balance' }}>New requests from your team show up here.</Text>
</div>
</div>
);
}Анатомия
Обзор
Icon — примитив из одной части, который передаёт размер и SVG-пропсы дочернему элементу через Slot.
Обязательные части
Icon
Обязательна. Владеет общим контрактом размерных токенов и пробросом SVG-пропсов.
Необязательные части
Экспортируемых дочерних частей нет.
Порядок композиции
Icon- один дочерний SVG
Корректные варианты композиции
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>
12 comments
</li>
<li style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-3)' }}>
<Icon size='sm'><PaperclipIcon /></Icon>
3 files
</li>
</ul>
);
}Некорректные варианты композиции
Дочернее содержимое, не являющееся SVG
Обёртка предназначена для проброса SVG-пропсов и размеров.
Самостоятельная смысловая иконка без подписи или контекста
Поведение aria-hidden по умолчанию может сделать её смысл недоступным.