Pull to refresh

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

Developer Soft corporate blog Website development *CSS *HTML *

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

Символический кот
Результат оборачивания кота в символ плагином gulp-svgstore. Собственно, почти ничего и не поменялось.
<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 xlink:href="#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 этого репозитория.
Надеюсь, кого-то заинтересовал этот рассказ.

UPD Добавил по результатам вопроса в комментариях ещё одну страничку, показывающую стилизацию 122-х настоящих иконок, всего 500 экземпляров на странице
Иконки для этого примера взяты на www.flaticon.com

Благодарности


При подготовке блог-поста не пострадало ни одно животное.
Котик, изначально растровый, был переведён в СВГ-термины в онлайн-редакторе (исходники).

Навык рисования котиков я получил, рисуя котиков своему сыну, который был ими чрезвычайно доволен.
Претензии же по поводу несимметричности ушей я принимать отказываюсь.

Список литературы


Очень-очень-очень подробный рассказ про стилизацию теневого SVG
Про SVG-иконки в принципе, а также о том, почему символ лучше группы
История темы иконок на хабре — SVG, Iconfonts vs PNG
SVG-иконки — разные подходы, тоже с котиками
Как правильно подгрузить SVG-спрайт в DOM
SVG библиотека рисования чартов. Никакого отношения к иконкам она не имеет, но там тоже котики, в том числе и моя кошка, (помогавшая мне писать статьи и служившая моделью для рисования).
Tags:
Hubs:
Total votes 61: ↑58 and ↓3 +55
Views 49K
Comments Comments 34

Information

Founded
1998
Location
Россия
Website
www.developersoft.ru
Employees
201–500 employees
Registered