Лучший подход к SVG иконкам

Источник: «The best approach to SVG icons»
SVG — лучший подход к реализации системы иконок в вебе. Существует множество способов использования SVG, каждый из которых имеет свои плюсы и минусы, и все их мы рассмотрим.

<img>

Преимуществом HTML элемента img является ленивая загрузка с помощью атрибута loading и приоритет ресурсов с помощью атрибута fetchpriority. Для сложного SVG с большим размером файла, который отображается ниже первой страницы, ленивая загрузка будет полезна.

Использование элемента img — простой подход, но предлагающий меньше контроля. Можно стилизовать иконку определёнными способами с CSS (opacity, filter), но отсутствует прямой контроль над такими вещами, как stroke-width, цвет fill или stroke, которые можно изменить, например, для hover, focus и disabled состояний или тёмного режима. SVG файл может содержать элемент <style>, но CSS функция light-dark() и медиа-запрос prefers-color-scheme не работают в Safari при использовании тега <img>.

SVG разметка в HTML

На противоположном конце спектра находится встроенный SVG. В отличие от других форматов изображений, код SVG можно вставить непосредственно в HTML. Такой подход обеспечивает максимальную гибкость. Можно выбрать разные части SVG для стилизации с CSS. Если нужен тонкий контроль, встроенный SVG — лучший вариант, но для большинства случаев использования иконок он даёт больше контроля, чем вам нужно.

У этого подхода есть и существенные недостатки. Одна SVG-иконка может состоять из большого количества разметки. Просмотр и редактирование HTML становится сложнее, когда его засоряют гигантские блоки кода SVG. Если необходимо изменить дизайн иконки, придётся вносить изменения во все места, где она используется.

SVG как компонент JavaScript-фреймворка

JavaScript-фреймворки, такие, как React, на первый взгляд, предлагают лучшее из двух миров. Абстрагировав SVG в JSX-компоненты, можно было сохранить гибкость стилизации встроенного SVG без необходимости просматривать массу SVG-кода каждый раз, когда мы просматриваем разметку страницы. У этого подхода есть недостатки с точки зрения производительности.

Подробнее о том, почему это плохая идея, читайте в статье Breaking Up with SVG-in-JS in 2023.

Есть ещё один недостаток использования JSX для SVG: вы не можете просто написать или скопировать/вставить обычную SVG разметку, потому что JSX требует атрибуты в camelCase. stroke-width становится strokeWidth, и т.д.

<use>

<use> — встроенная в браузер система компонентов SVG. Можно использовать currentColor и CSS переменные для достижения большей гибкости в оформлении, чем с HTML <img>.

Существуют разные подходы к использованию <use>:

Можно сочетать оба подхода. Если несколько иконок видны в верхней части каждой страницы, имеет смысл сократить количество HTTP-запросов, объединив их в один файл. Напротив, если иконка появляется только один раз в нижней части малопосещаемой страницы, её не стоит размещать в spritesheet.

Отдельные SVG-файлы легче поддерживать. Если больше не используете определённую иконку, проще удалить файл, чем копаться в разметке spritesheet.

Рассмотрим оба подхода, но независимо от способа хранения иконок, обращение к ним одинаковое:

<svg>
<use href="sprites.svg#icon-1"></use>
</svg>

При работе с элементом <use> одного пути к файлу недостаточно. Хэштег # (технически называемый идентификатор фрагмента) должен ссылаться на идентификатор в разметке SVG.

Если синтаксис <use> не кажется привлекательным, его легко абстрагировать в компонент во фреймворке JavaScript и передать href в качестве пропса.

Создание spritesheet с <symbol>

Можно хранить несколько иконок в одном .svg файле, обычно называемом SVG спрайтом или spritesheet. Внутри этого файла для определения каждой иконки используется элемент <symbol>:

<svg xmlns="http://www.w3.org/2000/svg">
<defs>

<symbol id="icon-1">
<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
<path d="M1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2zm13 1a.5.5 0 0 1 .5.5v6l-3.775-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12v.54L1 12.5v-9a.5.5 0 0 1 .5-.5z"/>
</symbol>

<symbol id="icon-2">
<path d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .491.592l-1.5 8A.5.5 0 0 1 13 12H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5M3.102 4l1.313 7h8.17l1.313-7zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4m7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4m-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2m7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
</symbol>

</defs>
</svg>

Элемент <symbol> используется для определения многократно используемых фрагментов SVG. Его содержимое рисуется только через <use> (если попытаться сослаться на <symbol> как на src <img>, ничего не отобразится). Если открыть spritesheet в таком инструменте, как Adobe Illustrator или Figma, то ничего не увидите.

<use> с отдельными SVG файлами

Элемент <symbol> удобен в сочетании с <use>, но не является необходимостью. <use> может ссылаться на любую часть SVG элемента — <path>, <g>, <circle>, даже на весь <svg>. Я предпочитаю хранить каждую отдельную иконку в отдельном .svg файле.

Необходимо вручную отредактировать каждый SVG файл, чтобы добавить id в элемент <svg>:

<svg id="icon" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="5"/>
</svg>

Спецификация SVG 2 позволяет отказаться от идентификатора фрагмента, что было бы большим улучшением, но пока это не поддерживается ни в одном браузере.

Благодаря тому, что иконки хранятся в виде отдельных SVG файлов, а не в виде spritesheet. Иконки по-прежнему легко использовать в качестве CSS background-image или src HTML тега <img>, а также можно открыть в дизайнерских программах. Можно использовать spritesheet для <img> и background-image, но код spritesheet становится немного сложнее.

Размер <use>

Без viewBox у SVG будет ширина 300px и высота 150px. Это произвольный размер, определённый в спецификации W3C. Данный размер будет таким независимо от внутренних пропорций изображения. Если задать иконке только width, высота не будет автоматически масштабироваться в зависимости от пропорций изображения: она останется равной 150px.

Если в SVG символ выглядит следующим образом:

<symbol id="tall-icon" viewBox="0 0 84 143">
<!-- код иконки... -->
</symbol>

К сожалению, при использовании <use> в HTML-файле необходимо снова определять viewBox:

<svg viewBox="0 0 84 143">
<use href="sprite.svg#tall-icon"></use>
</svg>

Когда viewBox задан, SVG будет масштабироваться в соответствии с корректным aspect-ratio, если задана только ширина или только высота.

В качестве альтернативы можно:

Стилизация <use>

<use> появился раньше веб-компонентов, но использует shadow DOM. Стилизация с помощью CSS более ограничена, чем в SVG, но можно использовать currentColor для изменения в SVG fill и stroke цвета, а также CSS переменные для стилизации всего остального.

В SVG разметке можно задать цвет stroke или fill как currentColor. Тогда иконка автоматически будет соответствовать цвету текста на странице, а цветом SVG можно легко управлять с помощью CSS свойства color.

currentColor хорошо работает, если нужно, чтобы вся иконка была одного цвета, а комбинируя его с opacity или синтаксисом относительного цвета, можно добиться многоцветности иконок.

Использование currentColor и opacity:

<svg id="plus" xmlns="http://www.w3.org/2000/svg" width="66" height="66" viewBox="0 0 24 24">
<path opacity=".2" fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Z"></path>
<path fill="currentColor" d="M12 7a1 1 0 0 0-.993.883L11 8v3H8a1 1 0 0 0-.117 1.993L8 13h3v3a1 1 0 0 0 1.993.117L13 16v-3h3a1 1 0 0 0 .117-1.993L16 11h-3V8a1 1 0 0 0-1-1Z"></path>
</svg>
<svg style="color: #00D3EF;">
<use href="/plus-icon-opacity.svg#plus"></use>
</svg>

<svg style="color: #FF289F;">
<use href="/plus-icon-opacity.svg#plus"></use>
</svg>

<svg style="color: #29EB6A;">
<use href="/plus-icon-opacity.svg#plus"></use>
</svg>

Использование currentColor с синтаксисом относительного цвета:

<svg id="plus" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="oklch(from currentcolor calc(L * 4) calc(C / 2) H)" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Z"></path>
<path fill="currentColor" d="M12 7a1 1 0 0 0-.993.883L11 8v3H8a1 1 0 0 0-.117 1.993L8 13h3v3a1 1 0 0 0 1.993.117L13 16v-3h3a1 1 0 0 0 .117-1.993L16 11h-3V8a1 1 0 0 0-1-1Z"></path>
</svg>

Что, если нужно стилизовать разные части SVG независимо друг от друга, явно задавая цвета? Или стилизовать что-то ещё, кроме цвета? Для такого случая единственным вариантом будут CSS переменные. В приведённом ниже примере при наведении курсора изменяется stroke-width за счёт обновления значения CSS переменной:

 <svg stroke-width="var(--stroke-width)" id="arrow" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" style="transition: stroke-width .4s;" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>

Использование одной иконки в нескольких вариантах

Удобно иметь возможность использовать одну и ту же иконку в разных вариантах. В одном контексте можно использовать <use>, а в другом может потребоваться background-image.

При использовании в <img> или background-image, SVG не имеет представления о currentColor или значении заданных CSS переменных. По умолчанию цвет fill для SVG — чёрный. Цвет stroke по умолчанию не задан. К счастью, функция var() принимает резервное значение, используемое, если переменная не задана, а также при работе с <img> или background-image:

stroke="var(--color1, #e040fb)"

В приведённом выше примере, если параметр --color1 не задан, stroke будет задан цвет #e040fb. Это отличный способ задать SVG цвет по умолчанию, но при этом дать возможность изменять его при необходимости с помощью CSS. Таким образом, сохраняется стилистический контроль при использовании иконки с <use> и возможность задать цвет, отличный от чёрного, при использовании с <img> или background-image.

Использование spritesheet с <img> и background-image

HTML <img> и CSS background-image не могут напрямую ссылаться на <symbol>. Однако можно использовать <use> внутри spritesheet, присвоить элементу <use> id, а затем ссылаться на этот id при указании src или url. Чтобы это работало, необходимо добавить в блок <style> несколько шаблонов display: none/display: block.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<style>
use {
display: none;
}
use:target {
display: block;
}
</style>
<symbol id="plus-icon" viewBox="0 0 512 512">
<path stroke="var(--color1, #bbdefb)" d="M448 256c0-106-86-192-192-192S64 150 64 256s86 192 192 192 192-86 192-192z" fill="none" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="var(--color2, #0d47a1)" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M256 176v160M336 256H176"/>
</symbol>

<use id="plus-icon-blue" href="#plus-icon" />
<use style="--color1: #b39ddb; --color2: #6200ea;" id="plus-icon-purple" href="#plus-icon" />
</svg>

Если нужно использовать <img> или background-image, но требуется одна и та же иконка в нескольких цветах, один из очевидных подходов — просто продублировать иконку в spritesheet и задать ей разные цвета stroke или fill. Однако есть и более эффективный подход, чем copy/paste. В приведённом выше примере одна и та же иконка экспортируется дважды: один раз со stroke с использованием оттенков синего, а другой — со strokes с использованием оттенков фиолетового.

В HTML-версии страницы можно просто использовать одну и ту же иконку в разных цветах:

<img src="circle.svg#plus-icon-blue" alt="">
<img src="circle.svg#plus-icon-purple" alt="">

mask-image

Если вам необходимо использовать SVG в качестве background-image, можно воспользоваться mask-image, позволяющей изменять цвет иконки:

.pink-heart {
mask-image: url(heart.svg);
background-color: pink;
}

.red-heart {
mask-image: url(heart.svg);
background-color: red;
}

А победителем становится…

Для большинства сценариев использования <use> — лучший вариант. Этот подход позволяет сбалансировать производительность, удобство для разработчиков и необходимую стилистическую универсальность.

Комментарии


Дополнительные материалы

Предыдущая Статья

Понимание битовых сдвигов JavaScript: << и >>

Следующая Статья

link rel='modulepreload': Оптимизация загрузки модулей JavaScript