Иконки

icon() возвращает встроенный <svg>. Все знаки нарисованы на одной сетке 24×24 незалитыми штрихами в currentColor, поэтому наследуют цвет и кегль того, в чём стоят, и не требуют собственного оформления.

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('check'),
  icon('search'),
  icon('trash'),
  icon('settings'),
)

Внутри компонента

Иконка — такой же потомок, как любой другой. Поскольку она задаётся в em, она подходит к подписи рядом, и говорить ей размер этой подписи не нужно:

stack({ direction: 'row', gap: 'sm', align: 'center', wrap: true },
  button({ color: 'primary' }, icon('download'), 'Скачать'),
  button({ variant: 'outline' }, icon('external-link'), 'Открыть'),
  button({ size: 'sm', variant: 'soft', color: 'danger' }, icon('trash'), 'Удалить'),
  iconButton({ label: 'Поиск', variant: 'soft', icon: icon('search') }),
)

Размер

По умолчанию 1em — размер окружающего текста. size принимает токен или любую CSS-длину, когда нужно от этого отойти:

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('star', { size: 'sm' }),
  icon('star'),
  icon('star', { size: 'lg' }),
  icon('star', { size: '2rem' }),
  icon('star', { size: '3rem' }),
)

Цвет

Пропа цвета нет. Иконка рисуется в currentColor, поэтому берёт цвет своего окружения — именно это позволяет одному набору работать внутри пяти палитр:

stack({ direction: 'row', gap: 'md', align: 'center' },
  text({ style: 'color: var(--su-primary)' }, icon('heart', { size: 'lg' })),
  text({ style: 'color: var(--su-success)' }, icon('check-circle', { size: 'lg' })),
  text({ style: 'color: var(--su-warning)' }, icon('alert-triangle', { size: 'lg' })),
  text({ style: 'color: var(--su-danger)' }, icon('x-circle', { size: 'lg' })),
  text({ tone: 'muted' }, icon('info', { size: 'lg' })),
)

Доступные имена

Иконка по умолчанию имеет aria-hidden, и это верно гораздо чаще, чем наоборот: иконку рядом со словом «Удалить» не нужно объявлять во второй раз. Давайте ей label только тогда, когда весь смысл несёт сама иконка, — и она станет role="img" с этим именем.

icon('trash')                       // декоративная — скрыта
button(icon('trash'), 'Удалить')    // говорит слово

icon('trash', { label: 'Удалить' }) // объявляется как изображение

// Кнопка с одной иконкой подписывает кнопку, а не знак внутри неё
iconButton({ label: 'Удалить', icon: icon('trash') })

Заливка

filled закрашивает знак вместо обводки. Контур в обоих случаях один и тот же — меняется только атрибут fill —, поэтому обе формы делят ровно одну внешнюю кромку и не могут разойтись.

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('bell', { size: 'lg' }),
  icon('bookmark', { size: 'lg' }),
  icon('folder', { size: 'lg' }),
  icon('heart', { size: 'lg' }),
  icon('star', { size: 'lg' }),
)

И те же имена, но с заливкой:

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('bell', { filled: true, size: 'lg' }),
  icon('bookmark', { filled: true, size: 'lg' }),
  icon('folder', { filled: true, size: 'lg' }),
  icon('heart', { filled: true, size: 'lg' }),
  icon('star', { filled: true, size: 'lg' }),
)

Это проп, а не второй набор имён, потому что залитость почти всегда — состояние: сохранено, понравилось, оценено, — а значит ей нужен булев флаг, а не другая строка:

icon('heart', { filled: liked })
icon('bookmark', { filled: saved, label: saved ? 'Сохранено' : 'Сохранить' })

// вместо
icon(liked ? 'heart-filled' : 'heart')

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

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('check-circle', { filled: true, size: 'lg' }),
  icon('x-circle', { filled: true, size: 'lg' }),
  icon('info', { filled: true, size: 'lg' }),
  icon('help', { filled: true, size: 'lg' }),
  icon('alert-triangle', { filled: true, size: 'lg' }),
)

У них есть второй рисунок — сплошная форма с меткой, вырезанной через fill-rule: evenodd, — потому что такую вырезку нельзя получить из контурного пути сменой одного атрибута. Внешняя форма рисуется по внешней кромке обводки, так что обе версии заканчиваются одним силуэтом. Проп в обоих случаях один и тот же; каким механизмом пользуется знак — его личное дело.

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

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('chevron-up', { filled: true, size: 'lg' }),
  icon('chevron-down', { filled: true, size: 'lg' }),
  icon('chevron-left', { filled: true, size: 'lg' }),
  icon('chevron-right', { filled: true, size: 'lg' }),
)

fillableIcons() перечисляет всё, что откликается на filled. Знак без залитой формы его игнорирует и остаётся контурным: залить eye значило бы потерять зрачок, а tag — его отверстие, так что ни один из них не притворяется.

Вращение

spin вращает знак — задумано для spinner, хотя ничто не мешает крутить refresh, пока что-то перезагружается. При prefers-reduced-motion он замедляется до еле заметного, а не останавливается: остановившийся спиннер выглядит сломанным.

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('spinner', { spin: true, size: 'lg' }),
  icon('refresh', { spin: true, size: 'lg' }),
  button({ variant: 'soft' }, icon('spinner', { spin: true }), 'Сохраняем…'),
)

Весь набор

Имена описывают рисунок, а не работу, которую он делает: x-circle, а не error — потому что один и тот же рисунок идёт на несвязанные задачи, и имя, описывающее картинку, при этом остаётся верным. Алиасы ниже покрывают привычные намерения.

alert-triangle
arrow-down
arrow-left
arrow-right
arrow-up
banknote
bell
bold
bookmark
calendar
camera
check
check-circle
chevron-down
chevron-left
chevron-right
chevron-up
chevrons-left
chevrons-right
clock
close
code
coins
comment-bubble
copy
credit-card
database
download
edit
external-link
eye
eye-off
facebook
feather
file
filter
folder
gear
gift
globe
google
heart
help
home
image
info
instagram
italic
key
layers
link
linkedin
location
lock
log-in
log-out
mail
menu
minus
moon
more-horizontal
more-vertical
package
percent
pin
plus
print
receipt
refresh
search
settings
share
shopping-bag
shopping-cart
smartphone
sparkles
spinner
star
store
sun
tag
terminal
thumbs-down
thumbs-up
tiktok
trash
truck
universal-access
unlock
upload
user
users
video
wallet
whatsapp
x-circle
x-twitter
youtube
zap

Бренды

С набором идут восемь фирменных знаков: facebook, google, instagram, linkedin, tiktok, whatsapp, x-twitter и youtube. Они по-прежнему принимают size и label и по-прежнему рисуются в currentColor:

stack({ direction: 'row', gap: 'md', align: 'center', wrap: true },
  icon('facebook', { size: 'lg' }),
  icon('instagram', { size: 'lg' }),
  icon('x-twitter', { size: 'lg' }),
  icon('youtube', { size: 'lg' }),
  icon('whatsapp', { size: 'lg' }),
  button({ variant: 'soft', color: 'neutral' }, icon('linkedin'), 'Поделиться'),
)

Это воспроизведения чужих знаков, а не рисунки в стиле этой библиотеки, поэтому они намеренно нарушают два её правила: они сплошные фигуры, а не штрихи, — именно таков логотип, — и пропорции у них фирменные, а не этой сетки. filled для них ничего не значит: они уже залиты.

Графика взята из Simple Icons, публикуемого под CC0. Это покрывает рисунок, но не товарный знак: используйте их, чтобы указать на то, что они называют, — ссылку на профиль, кнопку «Поделиться», — но не на собственном продукте.

Это x-twitter, а не x, потому что x уже алиас для close, и кнопка закрытия, превращающаяся в логотип, была бы неприятным сюрпризом. twitter тоже ведёт сюда.

Алиасы

Каждый из них рисует знак из списка выше — под тем именем, к которому вы, скорее всего, потянетесь:

success → check-circle

warning → alert-triangle

danger, error → x-circle

x, cross → close

question → help

loading → spinner

cog, gears → gear

delete, trash-can → trash

pencil → edit

notification → bell

dots → more-horizontal

bolt, lightning → zap

arrow-back → arrow-left

arrow-forward → arrow-right

cart → shopping-cart

bag → shopping-bag

card → credit-card

cash, money → banknote

delivery, shipping → truck

shop → store

discount, sale → percent

login, sign-in → log-in

logout, sign-out → log-out

map-pin, marker → location

mobile → smartphone

like → thumbs-up

dislike → thumbs-down

comment, message, chat → comment-bubble

ai, magic → sparkles

printer → print

accessibility, a11y → universal-access

twitter → x-twitter

Свои иконки

registerIcons() добавляет знак или заменяет встроенный. Разметка — это содержимое <svg>: фигуры на той же сетке 24×24, оставленные незалитыми, чтобы до них дошёл currentColor. Вызовите её один раз из модуля, который импортируют ваши страницы:

import { registerIcons } from 'sitelo/ui'

registerIcons({
  logo: '<path d="M4 20 12 4l8 16z"/>',
  // Уже существующее имя заменяет знак повсюду — так и переоформляют
  // встроенный, не форкая библиотеку.
  check: '<path d="m5 13 4 4 10-11"/>',
  // Одна замкнутая фигура, чтобы знак откликался на `filled`, как встроенные.
  pin: { markup: '<path d="M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11z"/>', fillable: true },
})
import { icon } from 'sitelo/ui'

icon('logo')                   // ваш знак
icon('check')                  // теперь тоже ваш

registerIcons({ check: null }) // и обратно к встроенному

Почему встроенный SVG, а не спрайт

Иконки рисуются прямо в странице, а не вытягиваются из icons.svg через <use>. Спрайт экономит порядка сотни гзипованных байт HTML на страницу и стоит за это одного похода по сети: повторяющаяся разметка — как раз тот случай, где gzip хорош, так что почти всё, ради чего спрайт и существует, уже сдедуплицировано. Встроенный вариант вдобавок означает, что нет файла, который нужно выдавать, нет базового пути, который нужно настраивать, и нечему пропасть из dist — та же сделка, что и у styles({ inline: true }).

Пропсы

ПропТипПо умолчаниюОписание
namestring—Какой знак. Можно передать и первым аргументом.
size'sm' | 'md' | 'lg' | string'md'Токен или любая CSS-длина. По умолчанию 1em.
labelstring—Объявлять как изображение с этим именем, а не скрывать.
spinbooleanfalseНепрерывно вращать.
filledbooleanfalseЗакрасить знак вместо обводки. Незаливаемые знаки это игнорируют.

Неизвестное имя не рисует ничего вместо того, чтобы бросать ошибку: косметический проп не должен уметь уронить сборку. hasIcon(name) говорит, существует ли такой знак, iconNames() перечисляет все, а fillableIcons() — те, что принимают filled.