Как использовать значки и управлять ими

Выбор техники создания значков

Хранение и отрисовка значков – это разные вещи. Например, спрайт SVG может предоставлять значок для системы значков CSS или на него можно ссылаться с помощью <use>.

  • Система значков CSS. Используйте, если CSS должен выбирать, когда и какой значок показывать. Используйте маску для одноцветного значка или фоновое изображение для полноцветного.
  • SVG с <use>. Используйте, если вам достаточно встроенных параметров стиля. CSS не может произвольно стилизовать или анимировать содержимое указанного SVG, но унаследованные и настраиваемые свойства могут быть переданы, если символ предназначен для их использования. Например, в контуре можно использовать fill="var(--icon-accent, currentColor)", чтобы CSS окрашивал эту часть значка.
  • Встроенное изображение SVG. Используйте, если вам нужно произвольное оформление или анимация содержимого SVG.
  • <img> с фильтрами CSS. Не используйте этот вариант для перекрашивания значков. Фильтры не могут надежно применить к изображению произвольное значение цвета CSS, поэтому перекрашивание является громоздким и негибким.

Чтобы принять решение, задайте два вопроса. Нужно ли CSS выбирать или менять значок (значения по умолчанию для компонентов, изменения состояния, темы)? Если да, используйте систему значков CSS. Нужно ли вам заглянуть внутрь значка, чтобы увидеть что-то, кроме цвета? Если да и вы управляете источником значка, создайте символ с дополнительными параметрами и используйте <use>. Если вы не управляете источником, встройте SVG. Сторонние наборы значков редко предоставляют доступ к дополнительным параметрам, поэтому при использовании <use> вы сможете изменить только цвет.

Встроенные или удаленные значки

Как система значков CSS, так и <use> могут брать значки из URI данных, отдельного файла или спрайта. Встраивание (URI данных или встроенный SVG) позволяет избежать запроса и отрисовывает значки при первой отрисовке, но раздувает CSS или HTML и не может быть кэшировано отдельно, поэтому подходит для небольшого набора значков, которые появляются на каждой странице. Отдельные файлы кешируются независимо и загружаются по запросу, каждый из которых обрабатывается отдельно. Спрайт – это один кешируемый запрос для всего набора, но он скачивает каждый значок, независимо от того, используется он или нет, а <use> может ссылаться на спрайт только из того же источника. Для нескольких основных значков лучше использовать встраивание, а для больших наборов – спрайт или отдельные файлы.

Если на странице одновременно отображаются тысячи значков, протестируйте саму страницу и объекты. В стресс-тесте встроенный SVG имел небольшое преимущество в производительности в таком масштабе, но результаты зависят от браузера и реализации. Стресс-тест значков SVG от Cloud Four

Основной подход: механизм значков на основе CSS

Значки – это токены дизайна. Каждый значок – это настраиваемое свойство, содержащее изображение, и любой элемент может отображать его, задав --icon-start или --icon-end. Система объединяет три возможности CSS:

  • Зарегистрированные дополнительные параметры (@property) не позволяют --icon-start и --icon-end наследовать значения, поэтому значок, заданный для компонента, не передается его дочерним элементам.
  • Запросы стилей контейнера (@container style(...)) определяют, когда элементу назначен значок, и отображают его без изменений класса или разметки.
  • Маски CSS и currentColor отображают значок в псевдоэлементе, который соответствует цвету текста элемента. Размер, цвет, интервалы и вертикальное смещение считываются из наследуемых свойств --icon-style-* с разумными резервными значениями, поэтому все поддерево можно переоформить, начиная с предка. Префикс --icon-style- предотвращает конфликт этих настроек с самими токенами значков, которые также находятся в --icon-*.

Определение токенов дизайна значков

Определите значки SVG и любые переопределения --icon-style-* как многократно используемые дополнительные параметры, используя встроенные URI данных SVG или относительные URL (например, url("/icons/trash.svg")). Если значок загружается из внешнего файла SVG, используйте относительный URL или встроенный URI данных. Относительный URL, используемый в дополнительном параметре, может разрешаться относительно таблицы стилей, в которой используется значок, а не таблицы стилей, в которой определен дополнительный параметр. Если эти таблицы стилей находятся в разных каталогах, URL может указывать на неправильный файл.

Чтобы URL оставался относительным по отношению к таблице стилей, которая определяет токен значка, используется зарегистрированное свойство: вычисленное значение зарегистрированного <url> или <image> разрешается по отношению к таблице стилей, в которой оно объявлено. НЕ полагайтесь на регистрацию токена как <image> с URL в initial-value. Chrome и Safari сопоставляют URL initial-value с документом, поэтому это работает только в Firefox. Chrome также разрешает значения, объявленные в :root, в отношении документа, если синтаксис не начинается с <url>.

Если относительных URL не избежать (например, когда таблицы стилей находятся в разных каталогах), зарегистрируйте токен с синтаксисом, начинающимся с <url>, и укажите фактический URL в :root. Это разрешается в отношении объявляющей таблицы стилей в каждом браузере:

@property --icon-caret {
  /* The syntax must start with <url>. A syntax that starts with <image> resolves against the document in Chrome. */
  syntax: "<url> | <image> | none";
  inherits: true;
  initial-value: none;
}

:root {
  /* Declared here, the URL resolves against this stylesheet */
  --icon-caret: url("icons/caret.svg");
}

Каждый значок теперь находится в двух местах, и шаблонного кода стало намного больше, поэтому такой подход оправдан, только если в проекте есть таблицы стилей в разных каталогах и управляемое количество значков. Если значков больше десятка, зарегистрируйте токены --icon-* (кроме --icon-style-*) из скрипта с помощью CSS.registerProperty, используя тот же синтаксис, вместо того чтобы вручную вводить каждый @property. Зарегистрированные свойства являются глобальными и не работают в теневых корнях, поэтому такой подход может подойти не для каждого проекта. Если вы можете использовать относительные URL или URI данных, то лучше выбрать их, поскольку они проще и работают везде.

:root {
  /* Optional: override the built-in fallbacks (1em, currentColor, 0.3em, -0.02em) */
  --icon-style-size: 1em;
  --icon-style-spacing: 0.4em;
  --icon-trash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6h16Z'/%3E%3C/svg%3E");
  --icon-favorite: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
}

Отрисовка значков из токенов

Чтобы показать значок, задайте для элемента значение --icon-start (в начале) или --icon-end (в конце) с одним из токенов выше. Два фрагмента CSS превращают это назначение в визуализированный значок.

Сначала зарегистрируйте --icon-start и --icon-end в @property. Синтаксис "*" принимает любое значение, и inherits: false – важная часть: без него набор значков на кнопке будет унаследован каждым потомком, и каждый из них также будет отображать значок.

@property --icon-start {
  syntax: "*";
  inherits: false;
}

@property --icon-end {
  syntax: "*";
  inherits: false;
}

Во-вторых, запрос стиля контейнера определяет, когда свойство задано для элемента, и отображает значок в псевдоэлементе. Поскольку псевдоэлемент не наследует --icon-start (он был зарегистрирован как ненаследуемый), первое объявление явно пересылает значение с помощью inherit. Остальные объявления управляют размером, маскированием, выравниванием и интервалами. Поскольку по умолчанию заливка равна currentColor, значок принимает цвет текста элемента и следует за его переходами.

/* Style query for elements configuring a start icon */
@container style(--icon-start) {
  ::before {
    --icon-start: inherit; /* Forward the value inside the query block */
    content: "" / var(--icon-style-alt, "");
    display: inline-block;
    width: var(--icon-style-size, 1em);
    height: var(--icon-style-size, 1em);
    background-color: var(--icon-style-color, currentColor);
    mask: var(--icon-start) no-repeat center / contain;
    /* Center on the cap height, nudged slightly up for optical balance */
    vertical-align: calc(0.5cap - var(--icon-style-size, 1em) / 2 + var(--icon-style-voffset, -0.02em));
    flex: none;
    pointer-events: none;
    forced-color-adjust: none;
  }

  /* Automatically add spacing ONLY if the element has other text or sibling content */
  :where(:not(.icon, :empty))::before {
    margin-inline-end: var(--icon-style-spacing, 0.3em);
  }
}

Каждое объявление занимает свое место:

  • vertical-align: middle выравнивается по половине высоты строчных букв, что выглядит слишком низко для большинства наборов значков. Центрирование по 0.5cap позволяет зафиксировать центр значка при увеличении --icon-style-size, а небольшое отрицательное значение --icon-style-voffset компенсирует то, что пространство над значком визуально кажется больше, чем под ним.
  • flex: none. Пустой псевдоэлемент имеет нулевой минимальный размер, поэтому внутри гибкой кнопки он сжимается в первую очередь, когда метка длинная, потенциально до нуля. Размер значка – это дизайнерское решение, а не то, что должно определяться давлением макета.
  • pointer-events: none – не позволяет значку перехватывать клики или становиться event.target.
  • forced-color-adjust: none: в режиме принудительного использования цветов браузер заменяет background-color, из-за чего маскированный значок исчезает. Заливка маски сохраняется, а цвет currentColor по-прежнему соответствует принудительному цвету текста.
  • content: "" / var(--icon-style-alt, "") – альтернативный текст для сгенерированного контента, который позволяет элементу управления, состоящему только из значка, получить доступное название из значка с помощью --icon-style-alt (см. раздел "Элементы управления, состоящие только из значка" ниже). Пустой атрибут alt по умолчанию скрывает декоративные значки от технологий специальных возможностей.

Для значка в конце используйте тот же шаблон с --icon-end и ::after вместо --icon-start и ::before. Используйте mask: var(--icon-end) и задавайте интервалы с помощью margin-inline-start вместо margin-inline-end.

Отдельные элементы и элементы, содержащие только значки

Для отдельных значков или кнопок, содержащих только значок, примените .icon непосредственно к пустому элементу (самой кнопке для элемента управления, содержащего только значок) и задайте его значок с помощью --icon. В этом примере --icon – свойство более высокого уровня, а правило .icon:empty сопоставляет его с --icon-start для начального значка:

.icon:empty {
  --icon-start: var(--icon);
}

Примеры использования

Настройки компонента по умолчанию в таблице стилей

Основное преимущество значков как дополнительных параметров заключается в том, что они участвуют в каскаде. Задайте значок для селектора, и он будет добавлен ко всем соответствующим элементам без изменения разметки:

.button-delete {
  --icon-start: var(--icon-trash);
}

a[href^="http"]:not([href*="example.com"]) {
  --icon-end: var(--icon-external);
}
<button class="button-delete">Delete item</button>

Изменение значков в зависимости от состояния

Поскольку значок – это просто свойство, изменение состояния – это обычное переопределение CSS. В этом случае нет второго элемента, который нужно показать или скрыть, и нет класса, который нужно переключить:

.button-expand {
  --icon-start: var(--icon-caret-right);
}

.button-expand[aria-expanded="true"] {
  --icon-start: var(--icon-caret-down);
}

То же самое относится к презентации: установите --icon-style-size, --icon-style-color или --icon-style-spacing для контейнера, например панели инструментов, и каждый значок внутри него обновится.

Отдельные значки со встроенными стилями

Элемент, который не охватывается ни одним правилом компонента, может задавать то же свойство в строке, например <button style="--icon-start: var(--icon-trash)">. Используйте этот способ только в крайнем случае. Если один и тот же значок появляется в элементе одного типа несколько раз, переместите его в правило таблицы стилей.

Элементы управления только со значками

Кнопки, содержащие только значок, используют класс .icon и --icon, описанные выше. Укажите доступное название кнопки с помощью aria-label или позвольте значку предоставить его через --icon-style-alt. Без этого значок, сгенерированный CSS, будет декоративным. Для отдельных информативных значков используйте элемент .icon с атрибутами role="img" и aria-label:

.button-close {
  --icon: var(--icon-close);
  --icon-style-alt: "Close";
}
<!-- Icon-only interactive control, icon and name from the stylesheet -->
<button class="icon button-close"></button>

<!-- Icon-only interactive control, named via aria-label -->
<button class="icon" style="--icon: var(--icon-trash);" aria-label="Delete item"></button>

<!-- Standalone informative icon -->
<span class="icon" style="--icon: var(--icon-favorite);" role="img" aria-label="Favorite"></span>

Работа с SVG напрямую

Если система значков CSS не подходит, используйте встроенный SVG и <use> вместе, а не в качестве альтернативы. Встройте SVG-код при первом появлении значка на странице и ссылайтесь на него с помощью <use href="#id"> при каждом повторе. Локальная ссылка не требует запроса и не имеет ограничений на перекрестное происхождение. Зарезервируйте внешний спрайт (<use href="/icons.svg#id">) для значков, которые используются на нескольких страницах.

При использовании <use> контент, на который ссылается элемент, клонируется в теневое дерево, поэтому CSS страницы не может выбрать его элементы. Наследуются следующие свойства: color, currentColor и любые дополнительные параметры. Помимо атрибутов представления SVG, современный CSS также может изменять многие атрибуты геометрии, поэтому значок, которым вы управляете, может предоставлять именно те хуки, которые вам нужны, например stroke-width="var(--icon-stroke, 2)" или даже d: var(--icon-path, path(…)) на определенном пути.

Свойство CSS d не поддерживается в Safari, поэтому используйте эту настройку, только если это не критично. Если вы не можете управлять значком, встройте его.

<use> важных фактов:

  • Элемент viewBox должен находиться в <symbol> или в ссылке <svg>. Внешний элемент <svg>, в который заключен <use>, не получает его из ссылки, поэтому задайте width и height в элементе-контейнере и оставьте viewBox с символом.
  • Стили не пересекают границу ни в одном направлении: <style> внутри внешнего спрайта не применяется к странице, а таблицы стилей страницы не применяются внутри дерева, на которое ссылается ссылка. Передаются только унаследованные и дополнительные параметры.
  • Внешние ссылки должны быть из того же источника. Спрайты из разных источников не поддерживаются.
  • Спрайты, встроенные в документ, используют пространство имен id страницы, поэтому идентификаторы символов пространства имен позволяют избежать конфликтов.

Отметьте декоративные SVG-элементы атрибутом aria-hidden="true" и задайте для содержащего их элемента управления доступное название, как в случае с системой значков CSS.

Резервные стратегии

Зарегистрированные дополнительные параметры

Без @property --icon-start и --icon-end ведут себя как обычные наследуемые дополнительные параметры, поэтому значок, установленный для элемента, также будет отображаться для его дочерних элементов. Чтобы избежать этого, сбросьте свойства дочерних элементов в запросе стиля (@container style(--icon-start) { * { --icon-start: initial; } }). Такой же подход используется в решении для Safari ниже.

Запросы стилей контейнера

Чтобы обеспечить поддержку браузеров без запросов стилей контейнеров, используйте классы или селекторы компонентов в качестве резервных хуков. Сохраните выбор значков в дополнительных параметрах. Селекторы встроенных стилей не учитывают значки, назначенные с помощью правил CSS.

:where(.icon-start, .icon:empty)::before {
  content: image-set(linear-gradient(transparent 0 100%), var(--icon-start)) / var(--icon-style-alt, "");
  /* icon styles */
}

:where(.icon-start)::before {
  --icon-start: inherit;
}

:where(.icon:empty)::before {
  --icon-start: var(--icon);
}

:where(.icon-start:not(.icon, :empty))::before {
  margin-inline-end: var(--icon-style-spacing, 0.3em);
}

Для конечных значков используйте тот же резервный класс-хук с .icon-end, --icon-end и ::after вместо .icon-start, --icon-start и ::before. Применяйте интервалы к непустому контенту с помощью margin-inline-start вместо margin-inline-end.

Этот шаблон image-set() делает content недействительным, когда дополнительный параметр не задан, поэтому он возвращается к none, а псевдоэлемент исчезает без добавления или удаления класса. Оно не удаляет другие стили псевдоэлемента и не предотвращает конфликты с другими правилами ::before или ::after, поэтому используйте эти хуки только там, где эти псевдоэлементы доступны для значков.

Обходные решения для браузера Safari

В Safari есть две ошибки рендеринга, влияющие на значки: WebKit #301609 может препятствовать их появлению до взаимодействия с пользователем, а #320220 также влияет на их рендеринг. Временное решение, описанное ниже, позволяет устранить обе проблемы.

@keyframes webkit-301609 {}

@supports ((-webkit-nbsp-mode: normal) or (-webkit-touch-callout: none) or (font: -apple-system-body)) and (content-visibility: auto) {
  @layer webkit-301609-fix {
    ::before, ::after {
      animation: webkit-301609 0s;
    }
  }
}

Запрос @supports предназначен только для Safari, а @layer уменьшает конфликты анимации.

Ошибка WebKit № 325900 может помешать прямому переопределению --icon-start в некоторых случаях, например при изменении значка при наведении. Это влияет на обходной путь для ненаследуемых зарегистрированных свойств. Для Safari зарегистрируйте --icon-start как наследуемое свойство и сбросьте его для дочерних элементов в запросе стиля:

@supports ((-webkit-nbsp-mode: normal) or
           (-webkit-touch-callout: none) or
           (font: -apple-system-body)) and (content-visibility: auto) {
  /* Override previous declaration */
  @property --icon-start {
    syntax: "*";
    inherits: true;
  }

  @layer webkit-325900-fix {
    @container style(--icon-start) {
      * {
        --icon-start: initial;
      }
    }
  }
}

Это предотвратит распространение значения значка на дочерние элементы. Если поместить его в слой, объявления значков без слоев будут иметь приоритет над сбросом.