Основания
Иконки
Inline SVG на сетке 16, обводка currentColor толщиной 1.5. Иконка наследует
цвет текста, поэтому в библиотеке нет ни одного иконочного варианта тона.
Разметка
<button class="inst-btn" type="button">
<svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
Обновить
</button>
<button class="inst-btn inst-btn--icon" type="button" aria-label="Настройки">
<svg class="inst-icon" aria-hidden="true"><use href="#i-settings"/></svg>
</button>
Контракт
Пять требований. Нарушение любого видно сразу — иконка либо съезжает по базовой линии, либо перестаёт менять цвет вместе с текстом.
| Требование | Почему |
|---|---|
Inline SVG (или <use> из спрайта) |
Иконка обязана менять цвет вместе с текстом. <img> и background-image этого не умеют |
stroke: currentColor, fill: none |
Один файл работает во всех пяти темах и во всех тонах без единого варианта |
| Толщина обводки 1.5 | Общая с хайрлайном оптика: 1 читается как царапина, 2 — как жирный шрифт рядом с 400 |
Сетка 16 (viewBox="0 0 16 16") |
Совпадает с --size-icon. Другая сетка даёт другую оптическую толщину при том же stroke-width |
| Цвет не живёт в data-URI | Форма рисуется маской, цвет приходит токеном. Иначе тема иконку не догоняет |
Класс .inst-icon объявляет это плюс flex: none — иначе иконка
сжимается первой, когда подпись рядом не помещается.
.inst-icon {
flex: none;
inline-size: var(--icon-size, var(--size-icon));
block-size: var(--icon-size, var(--size-icon));
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
Разметка
| Случай | Разметка |
|---|---|
| Иконка рядом с подписью | aria-hidden="true" на <svg>. Имя даёт подпись; без атрибута скринридер прочитает элемент дважды или невнятно |
| Иконка вместо подписи | aria-hidden="true" на <svg> и aria-label на кнопке. Кнопка без текста иначе не имеет доступного имени |
| Иконка внутри ссылки | То же самое: имя несёт ссылка |
| Иконка как единственный носитель информации | Так не бывает. Добавьте подпись или aria-label — цвет и форма не имеют права быть единственным носителем |
Доступность
| Декоративная иконка | aria-hidden="true" всегда, без исключений: имя даёт подпись или aria-label |
| Кнопка без текста | Обязана нести aria-label. Иначе у неё нет доступного имени вовсе |
| Контраст | Иконка наследует цвет текста, поэтому проходит те же пороги. Иконка на --text-faint — декорация и не имеет права нести смысл |
| Цвет не единственный носитель | Иконка сама по себе второй носитель тона (сноска), но не первый: тон обязан быть ещё и в тексте |
| Режим принудительных цветов | Обводка currentColor переживает сброс, потому что цвет берётся от текста, а не задан своим значением |
| Фокус | На <svg> фокуса не бывает: фокусируется кнопка или ссылка вокруг него, focusable="false" у спрайта убирает старый IE-артефакт |
Устройство
Спрайт объявляется один раз на документ, до первого <use>. Сам он невидим и
скрыт от скринридера.
<svg class="inst-sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
<symbol id="i-refresh" viewBox="0 0 16 16">…</symbol>
</svg>
Использование:
<svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>
У <svg> в месте использования нет собственного viewBox: он приходит из
<symbol>. Собственный viewBox на внешнем элементе перебивает символьный и
обрезает часть глифов.
Доступные id
Набор намеренно маленький: иконка заводится под задачу, а не «на всякий случай». Список печатается из самого спрайта — набранный руками он уже однажды разошёлся с библиотекой на четыре символа.
i-booki-charti-checki-chevroni-closei-copyi-enteri-gridi-listi-logouti-mici-morei-movei-plusi-refreshi-rotatei-scalei-settingsi-themei-usersПлюс 103 символов страниц справочника (i-p-*) — они принадлежат сайту, а не киту.
Размеры
| Класс или переменная | Размер | Токен | Где |
|---|---|---|---|
inst-icon |
18px | --size-icon |
Умолчание: кнопка, пункт меню, сноска, шапка |
inst-icon--sm |
10px | --size-chevron |
Шеврон селекта, крестик тега — служебный глиф |
inst-icon--down |
— | поворот 90deg |
Единственный шеврон, повёрнутый: аффорданс выпадающего списка на кнопке |
Какой размер берёт шеврон
--size-chevron — это 10px против 18 у --size-icon, и одно другим не
заменяется: токен отвечает на три разные задачи, и только две из них его
собственные.
| Задача | Размер | Почему | Где |
|---|---|---|---|
| Разделитель между словами | --size-chevron |
Стоит внутри строки текста и меряется по кеглю | Хлебные крошки, путь каскадера |
| Фурнитура рамки — раскрытие того, на чём стоит | --size-chevron |
Меряется по подписи рядом. Полоса 0.62…0.86 кегля: крупнее — и стрелка спорит со словами | Стрелка селекта, твист аккордеона, твист дерева, стрелка сортировки |
| Глиф в колонке хвостовых глифов | --icon-size |
Меряется по фактическому соседу, а сосед — другой глиф, а не подпись | Стрелка ветки каскадера: она стоит у того же края, что галочка меню |
Третья строка — та, где ошибиться легче всего. .inst-cascader-item носится
вместе с .inst-menu-item, у которого отметка выбора — --size-icon; десять
против восемнадцати в одной колонке читаются как два словаря, а не как один
список.
Шеврон, который сам является целью нажатия, — отдельный вопрос, и ответ на
него не мельче глиф, а крупнее бокс. .inst-tab-close это и формулирует:
квадрат из яруса контролов, а служебный глиф внутри, — потому что крестик в
10px это цель в 10px и заливка ховера размером со знак. Там, где цель
настоящая, ставится настоящая
inst-btn--xs: псевдоэлемент, нарисованный
под кнопку, — обещание, которого разметка не держит.
| --icon-size: var(--size-icon-sm) | 14px | --size-icon-sm | Показание в строке состояния: там кегль --text-2xs |
| --icon-size: var(--size-icon-lg) | 24px | --size-icon-lg | Значок в рельсе, где подписи нет вовсе |
Все четыре — одна мера с разными опорами. Значок рядом с подписью меряется
против ЕЁ прописной, поэтому у одиннадцатого кегля значок свой; значок без
подписи меряется против цели нажатия, потому что сравнивать не с чем. Проверяет
cmd/proportion, полосы там же.
Иконка крупнее этих четырёх — либо иллюстрация (её место в пустом состоянии), либо ошибка иерархии: иконка обслуживает данные и обязана быть тише их.
Размеры не реагируют на плотность намеренно — см. плотность: иконка носитель значения, и на 12px обводка 1.5 превращается в кляксу.
Композиции
<div class="inst-menu" role="menu">
<button class="inst-menu-item" type="button" role="menuitem">
<svg class="inst-icon" aria-hidden="true"><use href="#i-refresh"/></svg>Перезапустить
</button>
<button class="inst-menu-item" type="button" role="menuitem">
<svg class="inst-icon" aria-hidden="true"><use href="#i-copy"/></svg>Скопировать id
</button>
</div>
Иконочная кнопка квадратная: ширина берётся из той же высоты контрола, поэтому квадрат сохраняется во всех трёх размерах и во всех плотностях.
Правила
Что брать, а что нет
| Используйте | Возьмите другое |
|---|---|
| Иконка рядом с подписью — второй носитель смысла | Иконка вместо подписи в списке действий — в меню подпись обязательна, см. меню в поповере |
Иконка вместо подписи там, где действие общеизвестно и есть aria-label |
Иконка вместо статуса — статус несёт точку и слово, см. бейдж |
| Иконка как второй признак тона у сноски | Цвет как единственный носитель — общее правило. Тон описан в цвете |
<use href="#i-*"> из спрайта |
Текстовый глиф ⟳ или ▶ — разные метрики в разных шрифтах и риск подстановки эмодзи |
.inst-icon--sm в шевроне и мелком контроле |
Своя ширина инлайном — размер приходит из ролей |
--icon-size на контейнере, когда компоненту нужен свой размер |
Селектор потомка .мой-блок > .inst-icon — если ваш слой раньше kit.components, правило не сработает и не пожалуется |
Настройка
- Нарисуйте на сетке 16×16, только обводкой.
- Уберите
fill,stroke,stroke-widthиз самих путей — они придут из.inst-icon. - Скруглите концы и стыки не в файле, а классом:
stroke-linecapиstroke-linejoinуже там. - Положите в спрайт как
<symbol id="i-…" viewBox="0 0 16 16">. - Проверьте на тёмной теме: обводка на 1.5 в тёмной теме оптически тоньше, и слишком мелкая деталь исчезает.
API
| Имя | Что делает |
|---|---|
| класс | |
inst-sprite | Спрайт в теле страницы. Прячет его: без правила это видимый блок в 150px |
inst-icon | Базовый. Обязателен всегда |
| модификатор | |
inst-icon--sm | Уменьшение до --size-chevron |
inst-icon--down | Тот же шеврон, повёрнутый вниз: аффорданс выпадающего списка. По часовой от inline-end, поэтому в RTL он тоже смотрит вниз |
| Имя | Значение | Откуда |
|---|---|---|
| переменная | ||
--icon-sizeРазмер этого значка. Ставится на КОНТЕЙНЕР и наследуется вниз. Через переменную, а не через селектор потомка: раскладка лежит в более раннем слое, и селектор оттуда мёртв молча | var(--size-icon-xs) | задаёт приложение |
| токен | ||
--size-iconУмолчание: значок рядом с подписью базового кегля | 18px | база +4: масштаб |
--size-icon-smЗначок при кегле --text-2xs: строка состояния | 14px | база +4: масштаб |
--size-icon-lgЗначок БЕЗ подписи: рельс разделов. Меряется против цели нажатия | 24px | база +4: масштаб |
--size-chevronСлужебный глиф: шеврон, крестик | 10px | база +4: масштаб |
--stroke | 1px | одно на все ячейки |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument