SVG-иконки – много и со стилем

c6509c6a12964aab9472e4edb556575d.png
Маленький рассказ о том, как наша команда решила организовать иконки в грядущем проекте. Чуть-чуть исторического экскурса, взгляды по сторонам (на 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-разметки.
c6509c6a12964aab9472e4edb556575d.png

Код котика
<?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.

Про символ
Этот элемент и задуман как шаблонный. Весьма самодостаточен, по уровню возможностей – почти как маленький svg-файлик. Позволяет определять свои условные «размеры» — viewBox, к которым будут применяться координаты элементов. То есть у нас полностью уйдёт проблема сопоставления наших размеров с размерами иконки, что плюсит. Чуть-чуть подробностей. Сейчас поддерживается из коробки билдёжками для gulp/grunt.


Символический кот
Результат оборачивания кота в символ плагином gulp-svgstore. Собственно, почти ничего и не поменялось.
<?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; }


И теперь все коты, ссылающиеся на этот шаблон, станут оранжевохвосты.
4a266c2d7dfd4348b7193ab15019e616.png
Так, наш последний метод уже дал нам два из трёх пунктов наших хотелок.
А вот при попытке застилизовать одну конкретную иконку меня сначала ждало разочарование.

Удивиться истории про 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;
}


Так Барсик приобретает оранжевый окрас, а Васька — дымчато-белый.
5e30366fdad9488cbfc61f1b425acd65.png
(если частям и элементам его кошачьего SVG-тела не указано иного. Приоритет наследованных CSS-свойств довольно низок, ниже атрибутов и уж точно ниже других правил, примененных к темплейту – а темплейт живёт полностью в DOM, без всяких оговорок. Подробности чуть ниже)
А дальше мы можем уже использовать любые вариации.

.cat-house:hover #barsik use {
     fill: red;
}


Таким образом, один цвет мы уже можем кастомизить совершенно свободно и в полном соответствии с буквой CSS. Реализовать подсветку при наведении становится проще простого.
3823f3cc9f1e4c46b0af2f2b5e90c810.png

Порядок применения стилей, свойств и атрибутов


В этот момент с непривычки начинает становится непонятно, поэтому я постараюсь расписать ещё раз.
Для меня это было небольшим камнем преткновения.
Подход первый, без 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;
}


Заставит его забыть собственный цвет и начать брать его у предков.
Вот так выглядит пример целиком. Любые совпадения случайны.
cc9591bd687643a78d771d39db3a3f5f.png
Эллипс станет оранжевым.
В общем-то, на этом трюке и основана большая часть примеров.
Ещё могут быть inline-стили и !important, но там уже всё идёт по аналогии 

Ну и ещё немножечко магии

Стилизация второго цвета


С помощью хакотрюка, основанного на переменной SVG currentColor, мы можем кастомизить уже два цвета:

.tail {
    fill: inherit;
}

.body {
    fill: currentColor;
}

svg use {
    fill: brown;
    color: orange;
}

svg:hover use {
    fill: orange;
    color: brown;
} 


e63b367dea15427bb3aeeaa358f8ec65.png
Как-то так.

Трюк с невидимым котом


Ещё одна минорная вещь, которая, в принципе, может когда-то пригодиться — скрывать элементы изображения при некоторых условиях.
Напрямую мне этого добиться не удалось — у опции visibility так, всё или ничего…
Но мне удалось, с помощью тех же цветов, покрасить элемент в прозрачный цвет. Очень красиво, главное — правильно подобрать оттенок.
0bdecb5f199d42bd809d19b8d9b03036.png

Технические мелочи


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 библиотека рисования чартов. Никакого отношения к иконкам она не имеет, но там тоже котики, в том числе и моя кощка (помогавшая мне писать статьи и служившая моделью для рисования).

© Habrahabr.ru