Ikony

icon() zwraca liniowy <svg>. Każdy glif narysowany jest na tej samej siatce 24×24 jako niewypełnione kreski w currentColor, więc dziedziczy kolor i rozmiar czcionki tego, w czym siedzi, i nie potrzebuje własnych stylów.

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

W komponencie

Ikona jest dzieckiem jak każde inne. Ponieważ wymiaruje się w em, pasuje do etykiety obok bez informowania jej, jak duża ta etykieta jest:

stack({ direction: 'row', gap: 'sm', align: 'center', wrap: true },
  button({ color: 'primary' }, icon('download'), 'Pobierz'),
  button({ variant: 'outline' }, icon('external-link'), 'Otwórz'),
  button({ size: 'sm', variant: 'soft', color: 'danger' }, icon('trash'), 'Usuń'),
  iconButton({ label: 'Szukaj', variant: 'soft', icon: icon('search') }),
)

Rozmiar

Domyślnie 1em — rozmiar otaczającego tekstu. size przyjmuje token albo dowolną długość CSS, gdy chcesz się od niego oderwać:

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' }),
)

Kolor

Nie ma propsa koloru. Ikona rysowana jest w currentColor, więc bierze kolor swojego kontekstu — i to właśnie sprawia, że jeden zestaw działa w pięciu paletach:

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' })),
)

Dostępne nazwy

Ikona domyślnie ma aria-hidden, co jest słuszne znacznie częściej niż nie: ikona obok słowa „Usuń” nie powinna być zapowiadana po raz drugi. Daj jej label tylko wtedy, gdy to ikona niesie całe znaczenie — wtedy staje się role="img" z tą nazwą.

icon('trash')                      // ozdobna — ukryta
button(icon('trash'), 'Usuń')      // to słowo mówi

icon('trash', { label: 'Usuń' })   // zapowiadana jako obraz

// Przycisk z samą ikoną nazywa przycisk, a nie glif w środku
iconButton({ label: 'Usuń', icon: icon('trash') })

Wypełnione

filled zamalowuje glif, zamiast go obrysowywać. To ta sama ścieżka w obu przypadkach — zmienia się tylko atrybut fill — więc obie postaci dzielą dokładnie tę samą krawędź zewnętrzną i nie mogą się rozjechać.

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' }),
)

I te same nazwy wypełnione:

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' }),
)

To props, a nie drugi zestaw nazw, bo stan wypełnienia jest niemal zawsze stanem — zapisane, polubione, ocenione — więc chce wartości logicznej, a nie innego ciągu znaków:

icon('heart', { filled: liked })
icon('bookmark', { filled: saved, label: saved ? 'Zapisane' : 'Zapisz' })

// zamiast
icon(liked ? 'heart-filled' : 'heart')

Glify statusu wypełniają się inaczej, bo ich znak siedzi wewnątrz kształtu. Zamalowanie koła połknęłoby ptaszka, więc znak jest z niego zamiast tego wycinany:

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' }),
)

Te niosą drugi rysunek — kształt pełny ze znakiem wyciętym przez fill-rule: evenodd — bo wycięcia nie da się uzyskać ze ścieżki obrysu przez zmianę atrybutu. Zewnętrzny kształt rysowany jest na zewnętrznej krawędzi obrysu, więc obie postaci i tak kończą na tej samej sylwetce. Props jest ten sam w obu przypadkach; którego mechanizmu używa glif, to już jego sprawa.

Szewron nie ma żadnego wnętrza do zamalowania — to linia otwarta — więc wypełnia się do trójkąta opisanego jego trzema punktami, zachowując kreskę zaokrąglającą rogi:

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() wypisuje wszystko, co odpowiada na filled. Glif bez postaci wypełnionej ignoruje go i zostaje obrysowany — wypełnienie eye straciłoby źrenicę, a tag swój otwór, więc żaden z nich nie udaje.

Obrót

spin obraca glif — pomyślany dla spinner, choć nic nie stoi na przeszkodzie, żeby obracać refresh, gdy coś się przeładowuje. Pod prefers-reduced-motion zwalnia do pełzania, zamiast się zatrzymać, bo zatrzymany wskaźnik wygląda na zepsuty.

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 }), 'Zapisywanie…'),
)

Zestaw

Nazwy opisują rysunek, a nie zadanie, które wykonuje — x-circle, a nie error — bo ten sam rysunek bywa używany do niepowiązanych zadań, a nazwa opisująca obrazek zostaje wtedy prawdziwa. Aliasy poniżej pokrywają typowe intencje.

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

Marki

Z zestawem przychodzi osiem znaków marek — facebook, google, instagram, linkedin, tiktok, whatsapp, x-twitter i youtube. Nadal przyjmują size i label i nadal rysują się w 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'), 'Udostępnij'),
)

To reprodukcje cudzych znaków, a nie rysunki w stylu tej biblioteki, więc celowo łamią dwie jej zasady: są pełnymi kształtami, a nie kreskami, czym logo jest, i mają proporcje marki, a nie tej siatki. filled nic dla nich nie znaczy — już takie są.

Rysunki pochodzą z Simple Icons, które udostępnia je na CC0. To obejmuje rysunek, a nie znak towarowy: używaj ich, żeby wskazać rzecz, którą nazywają — odnośnik do profilu, przycisk udostępniania — a nie na własnym produkcie.

Jest to x-twitter, a nie x, bo x jest już aliasem close, a przycisk zamykania zmieniający się w logo byłby paskudną niespodzianką. twitter też się na nie rozwiązuje.

Aliasy

Każdy z nich renderuje glif wypisany powyżej, pod nazwą, po którą prędzej sięgniesz:

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

Własne ikony

registerIcons() dodaje glif albo zastępuje wbudowany. Znacznikami jest zawartość <svg> — kształty na tej samej siatce 24×24, zostawione niewypełnione, żeby sięgnął do nich currentColor. Wywołaj to raz z modułu, który importują Twoje strony:

import { registerIcons } from 'sitelo/ui'

registerIcons({
  logo: '<path d="M4 20 12 4l8 16z"/>',
  // Nazwa, która już istnieje, zastępuje ją wszędzie — tak przerysowuje
  // się wbudowany glif bez forkowania biblioteki.
  check: '<path d="m5 13 4 4 10-11"/>',
  // Jeden zamknięty kształt, żeby mógł odpowiadać na `filled` jak wbudowane.
  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')                  // Twój glif
icon('check')                 // teraz też Twój

registerIcons({ check: null }) // i z powrotem do wbudowanego

Dlaczego w treści, a nie sprite

Ikony renderują się do strony, zamiast być pobierane z icons.svg przez <use>. Sprite oszczędza rzędu setki bajtów HTML-a po gzipie na stronę i kosztuje za to jedną podróż do serwera — powtarzające się znaczniki to dokładnie ten przypadek, w którym gzip jest najlepszy, więc większość tego, co sprite miałby deduplikować, została już zdeduplikowana. W treści znaczy też, że nie ma pliku do wypuszczenia, ścieżki bazowej do skonfigurowania ani niczego, co mogłoby zginąć z dist — ten sam kompromis, który robi styles({ inline: true }).

Propsy

PropTypDomyślnieOpis
namestring—Który glif. Można podać jako pierwszy argument zamiast propsa.
size'sm' | 'md' | 'lg' | string'md'Token albo dowolna długość CSS. Domyślnie 1em.
labelstring—Zapowiadaj ją jako obraz o tej nazwie, zamiast ją ukrywać.
spinbooleanfalseObracaj ją bez przerwy.
filledbooleanfalseZamaluj glif, zamiast go obrysowywać. Ignorowane przez glify, których nie da się wypełnić.

Nieznana nazwa nie renderuje zupełnie nic, zamiast zgłaszać błąd — kosmetyczny props nie powinien móc przerwać buildu. hasIcon(name) mówi, czy dany istnieje, iconNames() wypisuje wszystkie, a fillableIcons() te, które przyjmują filled.