SVG-спрайт собирает все иконки в один файл: каждая лежит в нём как <symbol>, а на странице иконка превращается в короткую ссылку <use> на свой символ. Это один из старейших способов доставлять иконки. В React-проектах от него часто отказываются в пользу отдельного компонента на каждую иконку. Разберём, когда спрайт всё же выигрывает, как выглядит его разметка и какие три подвоха заставляют команды от него отказаться.
Коротко
- Спрайт не пускает разметку иконок в JavaScript-бандл. Компоненты кладут туда каждую импортированную иконку.
- Пишитеhref, а неxlink:href, и отдавайте спрайт с того же происхождения (origin), что и страницу.
- Красьте иконки спрайта черезcurrentColor: CSS-селекторы не проникают внутрь<use>, а наследуемые свойства проникают.
- Спрайт хорош, когда иконок на странице много или их используют не только в React. Компоненты нужны, когда у иконки своё поведение.
Как выглядит спрайт
Вот спрайт на три иконки, цвета в нём уже заменены на currentColor (условный пример, сокращён):
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
<symbol id="icon-arrow-right" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2"/></symbol>
<symbol id="icon-check-circle" viewBox="0 0 24 24">…</symbol>
<symbol id="icon-github" viewBox="0 0 16 16">…</symbol>
</svg>Символ <symbol> работает как шаблон. По словам MDN, он «не предназначен для отрисовки», отрисовываются только его экземпляры (MDN: symbol). У каждого символа свой viewBox, поэтому логотип на 16 пикселей и стрелка на 24 спокойно живут в одном файле. Префикс, здесь icon-, не даёт идентификаторам столкнуться с другими ID на странице.
Как им пользоваться
Иконка на странице ссылается на символ по ID:
<svg width="24" height="24" aria-hidden="true">
<use href="/icons-sprite.svg#icon-arrow-right" />
</svg>В React все иконки закрывает один небольшой компонент:
type IconName = "arrow-right" | "check-circle" | "github";
export function Icon({ name, label }: { name: IconName; label?: string }) {
return (
<svg
width="1em"
height="1em"
role={label ? "img" : undefined}
aria-label={label}
aria-hidden={label ? undefined : true}
>
<use href={`/icons-sprite.svg#icon-${name}`} />
</svg>
);
}Размер 1em заставляет иконку следовать за размером текста. Объединённый тип превращает опечатку в имени иконки в ошибку типов, а не в пустое место на странице. Генерируйте этот тип из того же списка, что и спрайт: тогда они не разойдутся.
Спрайт или компоненты
Когда выигрывает спрайт
Много иконок на странице. С компонентами каждая импортированная иконка становится JavaScript-кодом. Данные контуров попадают в бандл, и браузер разбирает и выполняет их до отрисовки. К тому же каждый экземпляр рисует полную разметку контуров, а экземпляр из спрайта остаётся одним элементом <use>. Цена JavaScript важна. Гросс на kurtextrem.de цитирует web.dev: «байт за байтом JavaScript обходится браузеру дороже, чем изображение или веб-шрифт того же размера» (Breaking Up with SVG-in-JS).
Потребители не на React. Спрайт работает в шаблоне с серверным рендерингом или на статичной странице без сборки. Один и тот же файл обслуживает их всех.
Стабильный общий набор. Набор, который меняется редко, остаётся одним файлом с долгим кэшем. Так поступили в Epic Stack, фулстек-стартере на React: по документации, он «использует SVG-спрайты ради максимальной производительности иконок» (Epic Stack docs: icons).
Когда выигрывают компоненты
- Своё поведение у иконки. Анимированной иконке или иконке, часть которой меняется при наведении, нужны внутренние элементы прямо в DOM страницы.
- Иконок всего несколько. Если на странице три иконки, компоненты после tree shaking стоят немного и избавляют от второго запроса.
- Многоцветные иконки. В спрайте они работают, но отдельными заливками вы управляете только через кастомные CSS-свойства внутри символа, например
fill="var(--icon-accent)".
Три подвоха
1. xlink:href устарел
MDN пишет прямо: «начиная с SVG 2 атрибут xlink:href устарел, вместо него используется href» (MDN: use). В старых примерах он всё ещё встречается. Пишите href; если указаны оба атрибута, побеждает href.
2. Отдавайте спрайт со своего происхождения
MDN формулирует осторожно: браузеры «могут применять политику одного источника к элементам <use> и отказываться загружать URL с другого источника». И ещё: «сейчас нет определённого способа задать политику кросс-доменного доступа для элементов <use>». Включить CORS нельзя. Спрайт на домене CDN может просто не загрузиться, и страница не покажет никакой ошибки. Отдавайте его с того же происхождения, что и страницу, или встройте спрайт в страницу один раз.
3. Селекторы не проникают внутрь
CSS-селекторы не находят элементы внутри копии, которую создаёт <use>. До них доходят только наследуемые свойства. У MDN это звучит так: свойства «не гарантированно наследуются клонированным DOM, если вы явно не запросите это через наследование CSS». Правило вроде .icon path { stroke: red } не сработает. Рисуйте иконки через currentColor и задавайте color внешнему <svg>: color наследуется, и иконка берёт его цвет.
Figma экспортирует тот цвет, которым рисовал дизайнер. Поэтому замена на currentColor происходит в вашей сборке. В нашем руководстве по иконкам из Figma в React этот шаг показан для компонентов.
Доступность
Декоративные иконки рядом с текстовой подписью получают aria-hidden="true", и это убирает их из дерева доступности (MDN: aria-hidden). Иконка, которая сама несёт смысл, например в кнопке без текста, получает role="img" и aria-label на внешнем <svg> (MDN: img role). Компонент Icon выше делает и то и другое по одному пропу.
Как получить спрайт из Figma
Сгенерировать спрайт просто. Сложно поддерживать его в актуальном виде. Каждая иконка, которую добавили, изменили или переименовали в Figma, должна попасть в файл с диффом, который можно проверить. Это тот же процесс ревью через pull request, что мы описали для токенов из Figma в код.
Мы делаем FISYCO, и он умеет доставлять библиотеку иконок из Figma в виде спрайта: один <symbol> на иконку, префикс на ваш выбор и pull request для ревью при каждом изменении иконок. Сейчас иконки в спрайте приходят с цветами, как их нарисовали в Figma; замена цветов работает только для компонентов. Чтобы перекрашивать иконки спрайта из CSS, пока добавьте шаг замены цветов в свою сборку. Нужны оба формата? Подключите один и тот же файл Figma дважды: как компоненты для одного репозитория и как спрайт для другого.



