Icons

icon() gibt ein inline <svg> zurück. Jedes Zeichen ist auf demselben 24×24-Raster als ungefüllte Striche in currentColor gezeichnet, erbt also Farbe und Schriftgröße von dem, worin es sitzt, und braucht keine eigenen Stile.

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

In einer Komponente

Ein Icon ist ein Kind wie jedes andere. Weil es sich in em bemisst, passt es zum Label daneben, ohne dass man ihm dessen Größe sagen müsste:

stack({ direction: 'row', gap: 'sm', align: 'center', wrap: true },
  button({ color: 'primary' }, icon('download'), 'Herunterladen'),
  button({ variant: 'outline' }, icon('external-link'), 'Öffnen'),
  button({ size: 'sm', variant: 'soft', color: 'danger' }, icon('trash'), 'Löschen'),
  iconButton({ label: 'Suchen', variant: 'soft', icon: icon('search') }),
)

Größe

Der Standard ist 1em — die Größe des umgebenden Textes. size nimmt ein Token oder jede CSS-Länge, wenn du davon abweichen willst:

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

Farbe

Es gibt keine Farb-Prop. Ein Icon wird in currentColor gezeichnet und nimmt damit die Farbe seines Kontexts an — genau das lässt einen Satz in fünf Paletten funktionieren:

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

Zugängliche Namen

Ein Icon ist standardmäßig aria-hidden, was weit häufiger richtig ist als nicht: ein Icon neben dem Wort „Löschen“ soll nicht ein zweites Mal angesagt werden. Gib ihm nur dann ein label, wenn das Icon die ganze Bedeutung trägt — dann wird es role="img" mit diesem Namen.

icon('trash')                       // dekorativ — versteckt
button(icon('trash'), 'Löschen')    // das Wort spricht

icon('trash', { label: 'Löschen' }) // als Bild angesagt

// Ein Icon-Button beschriftet den Button, nicht das Zeichen darin
iconButton({ label: 'Löschen', icon: icon('trash') })

Gefüllt

filled malt ein Zeichen aus, statt es zu umreißen. Es ist in beiden Fällen derselbe Pfad — nur das Attribut fill ändert sich —, sodass beide Formen exakt dieselbe Außenkante teilen und nicht auseinanderdriften können.

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

Und dieselben Namen, gefüllt:

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

Es ist eine Prop und kein zweiter Satz von Namen, weil der gefüllte Zustand fast immer ein Zustand ist — gespeichert, geliked, bewertet — und deshalb ein Boolean will, keinen anderen String:

icon('heart', { filled: liked })
icon('bookmark', { filled: saved, label: saved ? 'Gespeichert' : 'Speichern' })

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

Die Status-Zeichen füllen sich anders, weil ihre Marke innerhalb der Form sitzt. Den Kreis auszumalen würde das Häkchen schlucken, also wird die Marke stattdessen ausgestanzt:

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

Diese tragen eine zweite Zeichnung — die volle Form mit der per fill-rule: evenodd herausgeschnittenen Marke —, weil sich eine solche Aussparung nicht durch Ändern eines Attributs aus dem Umrisspfad gewinnen lässt. Die äußere Form wird an der Außenkante des Umrisses gezeichnet, sodass beide Fassungen auf derselben Silhouette enden. Es ist in beiden Fällen dieselbe Prop; welchen Mechanismus ein Zeichen nutzt, ist seine eigene Sache.

Ein Chevron hat gar kein Inneres zum Ausmalen — es ist eine offene Linie — und füllt sich daher zu dem Dreieck, das seine drei Punkte beschreiben, samt dem Strich, der die Ecken rundet:

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() listet alles, was auf filled hört. Ein Zeichen ohne gefüllte Form ignoriert es und bleibt umrissen — eye zu füllen würde die Pupille verlieren und tag sein Loch, also tut keines von beiden so.

Drehen

spin dreht das Zeichen — gedacht für spinner, auch wenn dich nichts daran hindert, refresh zu drehen, während etwas neu lädt. Unter prefers-reduced-motion wird es zum Schneckentempo statt anzuhalten, denn ein stehender Spinner sieht kaputt aus.

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

Der Satz

Namen beschreiben die Zeichnung, nicht ihren Zweck — x-circle, nicht error —, weil dieselbe Zeichnung für unzusammenhängende Zwecke benutzt wird und ein Name, der das Bild beschreibt, dabei wahr bleibt. Die Aliase unten decken die üblichen Absichten ab.

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

Marken

Acht Markenzeichen kommen mit dem Satz — facebook, google, instagram, linkedin, tiktok, whatsapp, x-twitter und youtube. Sie nehmen weiterhin size und label und zeichnen weiterhin in 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'), 'Teilen'),
)

Sie sind Reproduktionen fremder Marken und keine Zeichnungen im Stil dieser Bibliothek, brechen also absichtlich zwei ihrer Regeln: es sind volle Flächen statt Striche, denn genau das ist ein Logo, und ihre Proportionen sind die der Marke, nicht die dieses Rasters. filled bedeutet ihnen nichts — sie sind es bereits.

Die Zeichnungen stammen von Simple Icons, die sie unter CC0 veröffentlichen. Das deckt die Zeichnung ab, nicht die Marke: nutze sie, um auf das zu zeigen, was sie benennen — einen Profillink, einen Teilen-Button — und nicht auf einem eigenen Produkt.

Es heißt x-twitter, nicht x, weil x bereits ein Alias für close ist und ein Schließen-Button, der sich in ein Logo verwandelt, eine böse Überraschung wäre. twitter führt ebenfalls dorthin.

Aliase

Jeder davon rendert ein oben gelistetes Zeichen, unter dem Namen, zu dem du eher greifst:

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

Eigene Icons

registerIcons() fügt ein Zeichen hinzu oder ersetzt ein mitgeliefertes. Das Markup ist der Inhalt des <svg> — Formen auf demselben 24×24-Raster, ungefüllt gelassen, damit currentColor sie erreicht. Rufe es einmal aus einem Modul auf, das deine Seiten importieren:

import { registerIcons } from 'sitelo/ui'

registerIcons({
  logo: '<path d="M4 20 12 4l8 16z"/>',
  // Ein bereits vorhandener Name ersetzt es überall — so gestaltest du ein
  // mitgeliefertes Zeichen um, ohne die Bibliothek zu forken.
  check: '<path d="m5 13 4 4 10-11"/>',
  // Eine geschlossene Form, damit es wie die mitgelieferten auf `filled` hört.
  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')                   // dein Zeichen
icon('check')                  // jetzt ebenfalls deins

registerIcons({ check: null }) // und zurück zum mitgelieferten

Warum inline und kein Sprite

Icons werden in die Seite gerendert, statt aus einem icons.svg per <use> geholt zu werden. Ein Sprite spart in der Größenordnung von hundert gzippten HTML-Bytes pro Seite und kostet dafür einen Roundtrip — wiederholtes Markup ist genau der Fall, in dem gzip am besten ist, das meiste, wofür es ein Sprite gibt, ist also schon dedupliziert. Inline heißt außerdem: keine Datei zu erzeugen, kein Basispfad zu konfigurieren und nichts, das in dist fehlen kann — derselbe Handel, den styles({ inline: true }) eingeht.

Props

PropTypStandardBeschreibung
namestring—Welches Zeichen. Kann stattdessen als erstes Argument übergeben werden.
size'sm' | 'md' | 'lg' | string'md'Ein Token oder jede CSS-Länge. Standard ist 1em.
labelstring—Als Bild mit diesem Namen ansagen, statt es zu verstecken.
spinbooleanfalseEs fortlaufend drehen.
filledbooleanfalseDas Zeichen ausmalen statt umreißen. Von nicht füllbaren Zeichen ignoriert.

Ein unbekannter Name rendert gar nichts, statt zu werfen — eine kosmetische Prop soll keinen Build scheitern lassen können. hasIcon(name) sagt dir, ob es eines gibt, iconNames() listet sie alle, und fillableIcons() die, die filled annehmen.