SVG-иконки – много и со стилем
Маленький рассказ о том, как наша команда решила организовать иконки в грядущем проекте. Чуть-чуть исторического экскурса, взгляды по сторонам (на PNG и векторные шрифты) и рассказ о том, как мы всё-таки обустроились в итоге.
Иконки у нас используются, и активно – хорошо подобранная иконка заменяет слова и предложения (а фигово подобранной иконке можно сделать всплывающую подсказку, но не будем о грустном)
В общем, есть (и продолжают создаваться) иконки. Надо их положить на веб-страницу. Надо сделать это так, чтобы потом голова не болела про них весь остаток проекта и ещё пару лет в поддержке. Ну и есть дополнительные хотелки:
- хочется вектора. Ну, ладно, вектор – это средство, а не цель. Цель – не беспокоиться ВООБЩЕ об изменении размеров, ретина дисплеях, сохранении изображения в разных форматах для разных целей.
- хочется стилизации иконок. Потому что у нас из коробки как минимум два набора тем (светлая и тёмная), а то и контрастная, для людей с нестандартным зрением, а то и оранжевенькая какая-нибудь появится ближе к Новому году… В общем – одна и та же по сути иконка должна выглядеть слегка иначе в зависимости от выбранной на странице темы.
- хочется динамической стилизации иконок. Статики – нам мало. Этого хватает для скриншотиков и рекламных буклетиков, но не для живых пользователей. А мы хотели жизни! Мы хотели ховера! Мы хотели селекшена!!! И дизаблить, дизаблить их всех!.. Извините.
- НЕ хочется, чтобы в этом участвовал JavaScript в любой его форме и проявлении. Иконки – это внешний вид, а за него ответственный HTML + CSS. Ну, ладно, класс selected я готов навесить на элементы, но это последняя граница…
Есть и факторы, облегчающие задачу. Иконки сейчас (2015, осень, начинает снежить) в моде плоские, строгие. Если лет пять назад иконки пестрели, то сейчас это ушло под влиянием МС, Эппла, Материал Дизайна…
tl;dr Внимание. Следующие несколько разделов – это расплывание мыcлею по древу, причём вширь, обзор решений (в том числе – неудачных) и котик в разных ракурсах.
Кому хочется технических подробностей того, что же вышло в итоге – пожалуйте сюда.
Чем не удовлетворяли классические решения
- Мы можем сделать тучу png-иконок и класть их внутрь тэгов image. И подменять на JS-событиях наведения мыши. Когда я делал сайт на третьем курсе, этот метод реально работал. Хорошо было, и не надо было заботиться о множестве соединений с сервером. Может, когда круг замкнётся и HTTP 2.0 победит… а пока это накладно. Поехали дальше.
- PNG-спрайты + CSS/background-image – хорошо, но не хватает. Наши коллеги из DevExtreme жили так некоторое время… но упёрлись в необходимость хоть какой-нибудь стилизации. Ведь с PNG-иконкой беда, её даже в красный на клиенте не покрасишь! Они перешли на Font Icons, а мы?..
- Font Icons для написания приложения (не большой библиотеки, а именно приложеньица) оказался слегка неудобным в работе. Изменения иконок требуют некоторого обслуживания (сбор иконок, сохранение в файл шрифта, хинтинги всякие…) Если в интернете уже есть все нужные вам иконки в составе Font Awesome – лучше варинта и искать не стоит. Но если иконки нужны свои… мы пока пошли дальше.
- Unicode-символы для иконок. Сам пошутил, сам посмеялся.Хотя...
хотя не могу не отметить, что я оч люблю их использовать в девелопменте, до дизайна и релиза. Быстро, просто, Ctrl + C, Ctrl +V, font-size, color, :hover, .selected { color: } – и вот у вас прекрасная иконка с ховером и селекшеном бесплатно, без СМС…
- SVG + CSS/background-image – имеем нормальное масштабирование. Не имеем стилизации непосредственно иконки. Нет, мы можем это заворкараундить – hover-состояние в принципе делается изменение цвета бэкграунда, а disabled-состояние — в какой-то мере изменением opacity. Это рабочий вариант, такой была наша первая залитая в репозиторий версия. Но вот сам рисунок мы стилизовать не можем. Потому что он не в DOMe. А когда в доме чего-то нету – это беда…
К тому, что не является частью документа, не применишь стили CSS, не подкрасишь, не изменишь.
А давайте положим SVG в ДОМ?
Мы, конечно, можем это сделать. Скопипастить каждую иконку и вставить её туда, где она будет нужна, в каждый кусочек HTML-разметки.
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
<ellipse class="face" ry="55" rx="55" cy="120.5" cx="154.5" stroke-width="1.5" stroke="#000" fill="#fff" />
<ellipse class="ear" ry="21.5" rx="11" cy="58" cx="116.5" stroke-width="1.5" stroke="#000" fill="#fff" />
<ellipse class="ear" ry="22.5" rx="5.5" cy="53" cx="189" stroke-width="1.5" stroke="#000" fill="#fff" />
<g class="whiskers">
<line y2="90.5" x2="288.5" y1="115.5" x1="186.5" stroke-width="1.5" stroke="#000" fill="none" />
<line y2="134.5" x2="193.5" y1="139.5" x1="304.5" stroke-width="1.5" stroke="#000" fill="none" />
<line y2="146.5" x2="192.5" y1="167.5" x1="302.5" stroke-width="1.5" stroke="#000" fill="none" />
</g>
<g class="whiskers" transform="rotate(-185 70.50000000000001,139.00000000000003) ">
<line y2="100.5" x2="113.5" y1="125.5" x1="11.5" stroke-width="1.5" stroke="#000" fill="none" />
<line y2="144.5" x2="18.5" y1="149.5" x1="129.5" stroke-width="1.5" stroke="#000" fill="none" />
<line y2="156.5" x2="17.5" y1="177.5" x1="127.5" stroke-width="1.5" stroke="#000" fill="none" />
</g>
<ellipse fill="#fff" stroke="black" class="body" ry="55.5" rx="107.5" cy="221" cx="248" />
<ellipse fill="#fff" stroke="black" class="tail" ry="76" rx="8.5" cy="128.5" cx="353" />
<g class="legs" fill="darkgray">
<ellipse ry="54.5" rx="7.5" cy="290" cx="153" />
<ellipse ry="58" rx="9.5" cy="309.5" cx="190" />
<ellipse ry="48" rx="10" cy="291.5" cx="311.5" />
<ellipse ry="42.5" rx="8.5" cy="269" cx="342" />
</g>
<circle class="eye" r="8" cx="134.5" cy="94.5" fill="darkgreen"/>
<circle class="eye" r="8" cx="171.5" cy="94.5" fill="darkgreen"/>
<ellipse class="nose" ry="5.5" rx="5.5" cy="118" cx="153" fill="grey" />
<g class="checkmark" visibility="hidden">
<line y2="250" x2="270" y1="194.5" x1="234.5" stroke-width="3" stroke="#000" fill="none"/>
<line y2="250" x2="270" y1="170.5" x1="314.5" stroke-width="3" stroke="#000" fill="none"/>
</g>
</svg>
Сам котик лежит тут.
То есть вот это всё предлагается положить внутрь документа.
А если она поменяется, то перескопипастить во все места использования.
Или поднапрячься и написать для нашего проекта узкоспециальное решение, которое во время сборки, или во время деплоймента, или во время исполнения будет по магическим атрибуам понимать, что вот тут должна быть иконка, и инжектить (inject, не знаю, как перевести) содержимое одного файлика внутрь содержимого другого файлика.
Это очень весело звучит и очень напрягает в поддержке.
К сожалению или к счастью, нам в DevExpress этот путь не подходит по определению. Мы не пишем (почти) конечные программные решения, мы пишем то, что другие люди будут использовать в своих решениях.
Представив себе лица этих людей, которых бы мы попросили ручками инжектить наши SVG на их странички, мы резко перешли к следующему возможному решению…
А если положить в DOM, но чуть-чуть, а лучше – автоматически?
Ну то есть подсоединить к нашему документу некое хранилище, аналог спрайта с иконками, в котором лежали бы всеее иконки.
А там, где они нужны, мы бы сказали – а дай-ка нам, дорогой браузер, иконку kotik!
Да, так можно делать, и люди так делают. Собираем все наши иконки пофайлово и кладём их все внутрь одной большооой SVG, которая содержит в себе лишь шаблоны иконок. Технически для этого используется элемент symbol.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="kotik" viewBox="0 0 400 400">
<ellipse class="face" ry="55" rx="55" cy="120.5" cx="154.5" stroke-width="1.5" stroke="#000" fill="#fff" />
<ellipse class="ear" ry="21.5" rx="11" cy="58" cx="116.5" stroke-width="1.5" stroke="#000" fill="#fff" />
<ellipse class="ear" ry="22.5" rx="5.5" cy="53" cx="189" stroke-width="1.5" stroke="#000" fill="#fff" />
<g class="whiskers">
<line y2="90.5" x2="288.5" y1="115.5" x1="186.5" stroke-width="1.5" stroke="#000" fill="none" />
<line y2="134.5" x2="193.5" y1="139.5" x1="304.5" stroke-width="1.5" stroke="#000" fill="none" />
<line y2="146.5" x2="192.5" y1="167.5" x1="302.5" stroke-width="1.5" stroke="#000" fill="none" />
</g>
<g class="whiskers" transform="rotate(-185 70.50000000000001,139.00000000000003) ">
<line y2="100.5" x2="113.5" y1="125.5" x1="11.5" stroke-width="1.5" stroke="#000" fill="none" />
<line y2="144.5" x2="18.5" y1="149.5" x1="129.5" stroke-width="1.5" stroke="#000" fill="none" />
<line y2="156.5" x2="17.5" y1="177.5" x1="127.5" stroke-width="1.5" stroke="#000" fill="none" />
</g>
<ellipse fill="#fff" stroke="black" class="body" ry="55.5" rx="107.5" cy="221" cx="248" />
<ellipse fill="#fff" stroke="black" class="tail" ry="76" rx="8.5" cy="128.5" cx="353" />
<g class="legs" fill="darkgray">
<ellipse ry="54.5" rx="7.5" cy="290" cx="153" />
<ellipse ry="58" rx="9.5" cy="309.5" cx="190" />
<ellipse ry="48" rx="10" cy="291.5" cx="311.5" />
<ellipse ry="42.5" rx="8.5" cy="269" cx="342" />
</g>
<circle class="eye" r="8" cx="134.5" cy="94.5" fill="darkgreen" />
<circle class="eye" r="8" cx="171.5" cy="94.5" fill="darkgreen" />
<ellipse class="nose" ry="5.5" rx="5.5" cy="118" cx="153" fill="grey" />
<g class="checkmark" visibility="hidden">
<line y2="250" x2="270" y1="194.5" x1="234.5" stroke-width="3" stroke="#000" fill="none" />
<line y2="250" x2="270" y1="170.5" x1="314.5" stroke-width="3" stroke="#000" fill="none" />
</g>
</symbol>
<symbol id="shapes" viewBox="0 0 400 400">
<circle fill="green" cx="200" cy="200" r="30" />
<g fill="darkblue">
<circle cx="100" cy="100" r="25" />
<circle cx="300" cy="100" r="25" />
</g>
<g fill="orangered">
<ellipse fill="violet" cx="200" cy="300" rx="200" ry="25" />
</g>
</symbol>
</svg>
Этот элемент нигде не отображается – by design. Он всего лишь маска для будущих настоящих иконок.
Мы можем взять этот шаблон и положить его на страницу, сославшись на него из элемента use.
<svg>
<use xlink:href="#kotik" />
</svg>
Всё, котик тут.
Белохвостый, обычный.
А ещё можем сделать интереснее. Шаблон – он уже дома. В смысле в DOMе. И его уже можно стилизовать совсем как настоящий. Используя имена svg-элементов (path
, circle
, rect
и т.д.), мы можем применять к ним CSS-правила и модифицировать атрибуты (цвета через fill
и stroke
, толщины и стиль линий)
.tail { color: orange; }
И теперь все коты, ссылающиеся на этот шаблон, станут оранжевохвосты.
Так, наш последний метод уже дал нам два из трёх пунктов наших хотелок.
А вот при попытке застилизовать одну конкретную иконку меня сначала ждало разочарование.
Удивиться истории про use, xlink: href и деревья вместе
Как только мы пытаемся кастомизить одного взятого котика в лоб – всё перестаёт работать.
<svg id="barsik">
<use xlink:href="#kotik"/>
</svg>
#barsik .tail {
fill: orangered;
}
Увы, хвост Барсика не рыжеет.
Причина этого интересна. Барсика на самом деле там нет.
Барсик – в тени.
ДОМ теней
Shadow DOM – штука уже как бы не новая. Её начали использовать производители браузеров для того, чтобы слепить input type=datebox, а мы об этом и не знали. В тот момент они ещё не были в курсе, что используют Shadow DOM, имя и форму он обрел несколько позже…
Элемент use – в современных терминах – использует именно что Shadow DOM.
Элементы, которые мы *клонируем* внутрь *корня* use – помещаются в наш DOM… но не до конца.
В частности, к ним нельзя применить CSS правила.
Но корень теневого дерева, элемент <use />
– он как бэ с одной стороны в нашем основном дереве, а с другой – является предком всего, что в дереве лежит.
С первой стороны мы применим к нему CSS-стиль
Со второй стороны – он пробросит приданные ему CSS-свойства своим потомкам. Если мы скажем шаблону, чтобы он к этому прислушался.
//явно указываем на уровне шаблона, что хвост должен унаследовать цвет заливки от своего предка
.tail {
fill: inherit;
}
//стилизуем предка
#barsik use {
fill: orangered;
}
Так Барсик приобретает оранжевый окрас, а Васька — дымчато-белый.
(если частям и элементам его кошачьего SVG-тела не указано иного. Приоритет наследованных CSS-свойств довольно низок, ниже атрибутов и уж точно ниже других правил, примененных к темплейту – а темплейт живёт полностью в DOM, без всяких оговорок. Подробности чуть ниже)
А дальше мы можем уже использовать любые вариации.
.cat-house:hover #barsik use {
fill: red;
}
Таким образом, один цвет мы уже можем кастомизить совершенно свободно и в полном соответствии с буквой CSS. Реализовать подсветку при наведении становится проще простого.
Порядок применения стилей, свойств и атрибутов
В этот момент с непривычки начинает становится непонятно, поэтому я постараюсь расписать ещё раз.
Для меня это было небольшим камнем преткновения.
Подход первый, без CSS
У SVG-элемента может быть атрибут, указывающий его цвет. Для большинства фигур (path, circle, ellipsis) это fill, реже используется stroke (для указания цвета границ фигуры.
<circle fill="green" cx="200" cy="200" r="30"></circle>
Если у элемента этого атрибута нет – он пытается получить его у предков. Идёт вверх по своим родителям до тех пор, пока не находит кого-то с указанным атрибутом.
<g fill="darkblue">
<circle cx="100" cy="100" r="25"></circle>
<circle cx="300" cy="100" r="25"></circle>
</g>
Два круга, заливка не указана, берут у родителя. Очень удачно – у первого же родителя, группы, в которой они лежат, есть заливка. Оба круга становятся синими.
Подход второй. Появление стилизации
Как только мы применяем стили – они побеждают. Правило
circle {
fill: orange;
}
Сделает оранжевыми оба круга. Оно просто сильнее…
Подход третий. Стилизация убивает атрибуты, чтобы элементы использовали атрибуты
<g fill="orangered">
<ellipse fill="violet" cx="200" cy="300" rx="200" ry="25"/>
</g>
Вот такой код даст нам фиолетовый эллипс.
Однако добавление CSS-правила
ellipse {
fill: inherit;
}
Заставит его забыть собственный цвет и начать брать его у предков.
Вот так выглядит пример целиком. Любые совпадения случайны.
Эллипс станет оранжевым.
В общем-то, на этом трюке и основана большая часть примеров.
Ещё могут быть inline-стили и !important, но там уже всё идёт по аналогии
Ну и ещё немножечко магии
Стилизация второго цвета
С помощью хакотрюка, основанного на переменной SVG currentColor, мы можем кастомизить уже два цвета:
.tail {
fill: inherit;
}
.body {
fill: currentColor;
}
svg use {
fill: brown;
color: orange;
}
svg:hover use {
fill: orange;
color: brown;
}
Как-то так.
Трюк с невидимым котом
Ещё одна минорная вещь, которая, в принципе, может когда-то пригодиться — скрывать элементы изображения при некоторых условиях.
Напрямую мне этого добиться не удалось — у опции visibility
так, всё или ничего…
Но мне удалось, с помощью тех же цветов, покрасить элемент в прозрачный цвет. Очень красиво, главное — правильно подобрать оттенок.
Технические мелочи
SVG-спрайт должен лежать в DOMe. Большинство браузеров смогли бы показать шаблоны и из залинкованного файла, но не ИЕ. Поэтому – выкачиваем через AJAX и вставляем в ДОМ. Есть статья.
Движки шаблонов могут работать странно. Атрибут xlink:href живёт в своём неймспейсе (собственно, в неймспейсе xlink, там так и написано), и это не все любят. Например, Knockout не умеет это биндить из коробки. Есть воркараунд. Он работает.
Могу предположить, что в других движках темплейтов могут возникнуть схожие проблемы. Могу предположить, что их можно решить схожим образом.
Что же получилось в итоге
- Наши иконки хранятся и лежат в отдельных SVG файликах, маленьких, любимых в VCS, понятных в диффах и открываемых в браузере
- Наши иконки в момент билдёжки собираются внутрь одного большого файлика – icons.svg. То, что было файликом, становится шаблоном.
- Этот файлик нужно положить внутрь HTML-разметки. Руками, инструментом сборки или JavaScript
- На иконки можно ссылаться с помощью вот такой конструкции:
<svg> <use href:xlink="#kotik"/> </svg>
Конструкция является более явной и более семантически верной, чем при создании иконки при помощи background-image - Шаблон иконки лежит внутри DOM и может быть модифицирован через CSS
path.tail { fill: darkgrey; }
- Конкретная иконка делается на основе шаблона, сама в DOM не попадает, но попадает в теневой DOM и нё можно стилизовать благодаря наследованию CSS-свойств от родительского элемента-якоря use.
#dvorovayBreed.selected use { fill: darkgreen; }
- Иконку можно стилизовать как угодно на уровне документа – задавать внутренним элементам SVG классы и красить/модифицировать для всего документа как душе угодно и как CSS позволяет
- Для одной конкретной иконки можно модифицировать два цвета – ровно два. Один – цивилизованно, второй – благодаря магии. Магия очень сильная и работает везде, но магия имеет свою цену – будьте аккуратны в продакшене!
- JavaScript нужен максимум один раз, в процессе жизни страницы больше не используется
Решение получилось не привязанным чреземерно к специфике билдёжке, хорошо стилизуемым.
Работает в IE9+, вебкитах, на читалке Amazon Kindle и на телевизоре Samsung.
Все примеры стилизации SVG-иконок лежат на Гитхабе. Я давал ссылки на GitHub Pages этого репозитория.
Надеюсь, кого-то заинтересовал этот рассказ.
Благодарности
При подготовке блог-поста не пострадало ни одно животное.
Котик, изначально растровый, был переведён в СВГ-термины в онлайн-редакторе (исходники).
Навык рисования котиков я получил, рисуя котиков своему сыну, который был ими чрезвычайно доволен.
Претензии же по поводу несимметричности ушей я принимать отказываюсь.
Список литературы
Очень-очень-очень подробный рассказ про стилизацию теневого SVG
Про SVG-иконки в принципе, а также о том, почему символ лучше группы
История темы иконок на хабре — SVG, Iconfonts vs PNG
SVG-иконки — разные подходы, тоже с котиками
Как правильно подгрузить SVG-спрайт в DOM
SVG библиотека рисования чартов. Никакого отношения к иконкам она не имеет, но там тоже котики, в том числе и моя кощка (помогавшая мне писать статьи и служившая моделью для рисования).