Лучший подход к 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-кода каждый раз, когда мы просматриваем разметку страницы. У этого подхода есть недостатки с точки зрения производительности.
Please don't import SVGs as JSX. It's the most expensive form of sprite sheet: costs a minimum of 3x more than other techniques, and hurts both runtime (rendering) performance and memory usage.
— Jason Miller 🦊⚛ (@_developit) April 15, 2021
This bundle from a popular site is almost 50% SVG icons (250kb), and most are unused. pic.twitter.com/G1IgOjTeIT
Подробнее о том, почему это плохая идея, читайте в статье Breaking Up with SVG-in-JS in 2023.
Yeah, we've done lots of research on this in the Preact team. Strings are always the number 1 reason for bundle size bloat.
— Marvin Hagemeister ⚛️ (@marvinhagemeist) November 10, 2023
And yes it's truly bonkers how much strings end up in js files. From icon sets, to base64 fonts, to inlined images, etc. Often makes up 60% of bundle size
Есть ещё один недостаток использования JSX для SVG: вы не можете просто написать или скопировать/вставить обычную SVG разметку, потому что JSX требует атрибуты в camelCase. stroke-width
становится strokeWidth
, и т.д.
<use>
<use>
— встроенная в браузер система компонентов SVG. Можно использовать currentColor
и CSS переменные для достижения большей гибкости в оформлении, чем с HTML <img>
.
Существуют разные подходы к использованию <use>
:
- Хранить каждую иконку в отдельном
.svg
файле и добавлятьid
к каждому<svg>
. - Использовать spritesheet файле, содержащий несколько иконок, с помощью
<symbol>
.
Можно сочетать оба подхода. Если несколько иконок видны в верхней части каждой страницы, имеет смысл сократить количество 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
, если задана только ширина или только высота.
В качестве альтернативы можно:
- Указать атрибуты
width
иheight
или задатьwidth
иheight
с помощью CSS. - Установить
aspect-ratio
в CSS.
Стилизация <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>
— лучший вариант. Этот подход позволяет сбалансировать производительность, удобство для разработчиков и необходимую стилистическую универсальность.