Iconos

icon() devuelve un <svg> en línea. Todos los glifos están dibujados sobre la misma retícula de 24×24 como trazos sin relleno en currentColor, así que heredan el color y el tamaño de letra de aquello donde estén y no necesitan estilos propios.

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

En un componente

Un icono es un hijo como cualquier otro. Como se dimensiona en em, encaja con la etiqueta que tiene al lado sin que haya que decirle cuánto mide esa etiqueta:

stack({ direction: 'row', gap: 'sm', align: 'center', wrap: true },
  button({ color: 'primary' }, icon('download'), 'Descargar'),
  button({ variant: 'outline' }, icon('external-link'), 'Abrir'),
  button({ size: 'sm', variant: 'soft', color: 'danger' }, icon('trash'), 'Eliminar'),
  iconButton({ label: 'Buscar', variant: 'soft', icon: icon('search') }),
)

Tamaño

Por defecto es 1em, el tamaño del texto que lo rodea. size admite un token o cualquier longitud CSS cuando quieres apartarte de eso:

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

Color

No hay prop de color. Un icono se dibuja en currentColor, así que toma el color de su contexto, que es lo que hace que un solo conjunto funcione dentro de cinco paletas:

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

Nombres accesibles

Un icono es aria-hidden por defecto, que es lo correcto muchísimo más a menudo que no: un icono junto a la palabra «Eliminar» no debería anunciarse por segunda vez. Dale un label solo cuando el icono cargue con todo el significado, y pasará a ser role="img" con ese nombre.

icon('trash')                        // decorativo — oculto
button(icon('trash'), 'Eliminar')    // la palabra es la que habla

icon('trash', { label: 'Eliminar' }) // se anuncia como imagen

// Un botón de solo icono etiqueta el botón, no el glifo de dentro
iconButton({ label: 'Eliminar', icon: icon('trash') })

Relleno

filled pinta un glifo en vez de perfilarlo. Es el mismo trazado en ambos casos — solo cambia el atributo fill —, así que las dos formas comparten borde exterior exactamente y no pueden separarse.

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

Y los mismos nombres, rellenos:

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 una prop y no un segundo conjunto de nombres porque el estado relleno casi siempre es un estado — guardado, con me gusta, valorado —, así que pide un booleano y no otra cadena:

icon('heart', { filled: liked })
icon('bookmark', { filled: saved, label: saved ? 'Guardado' : 'Guardar' })

// en vez de
icon(liked ? 'heart-filled' : 'heart')

Los glifos de estado se rellenan de otra manera, porque su marca queda dentro de la forma. Pintar el círculo se tragaría la marca de verificación, así que en su lugar se recorta:

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

Esos llevan un segundo dibujo — la forma maciza con la marca recortada mediante fill-rule: evenodd —, porque un recorte no se consigue desde el trazado del perfil cambiando un atributo. La forma exterior se dibuja en el borde exterior del perfil, así que ambas versiones acaban con la misma silueta. Es la misma prop en cualquier caso; qué mecanismo usa cada glifo es asunto suyo.

Un chevrón no tiene interior que pintar — es una línea abierta —, así que se rellena hasta el triángulo que describen sus tres puntos, conservando el trazo que redondea las esquinas:

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() lista todo lo que responde a filled. Un glifo sin forma rellena lo ignora y se queda perfilado: rellenar eye perdería la pupila y tag su agujero, así que ninguno finge lo contrario.

Giro

spin rota el glifo — pensado para spinner, aunque nada te impide girar refresh mientras algo se recarga. Baja a paso de tortuga en vez de detenerse bajo prefers-reduced-motion, porque un spinner detenido parece roto.

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

El conjunto

Los nombres describen el dibujo y no el trabajo que hace — x-circle, no error — porque el mismo dibujo se usa para trabajos que no tienen que ver entre sí, y un nombre que describe la imagen sigue siendo cierto cuando eso pasa. Los alias de abajo cubren las intenciones habituales.

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

Marcas

Con el conjunto vienen ocho marcas: facebook, google, instagram, linkedin, tiktok, whatsapp, x-twitter e youtube. Siguen admitiendo size y label y se siguen dibujando en 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'), 'Compartir'),
)

Son reproducciones de marcas ajenas y no dibujos al estilo de esta biblioteca, así que rompen dos de sus reglas a propósito: son formas macizas en vez de trazos, que es lo que es un logotipo, y sus proporciones son las de la marca y no las de esta retícula. filled no significa nada para ellas: ya lo están.

El arte viene de Simple Icons, que lo publica bajo CC0. Eso cubre el dibujo, no la marca registrada: úsalos para señalar aquello que nombran —un enlace a un perfil, un botón de compartir— y no en un producto tuyo.

Es x-twitter y no x, porque x ya es alias de close y que un botón de cerrar se convirtiera en un logotipo sería una sorpresa desagradable. twitter también resuelve a él.

Alias

Cada uno de estos dibuja un glifo de los de arriba, bajo el nombre al que es más probable que recurras:

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

Tus propios iconos

registerIcons() añade un glifo, o reemplaza uno de serie. El marcado es el contenido del <svg>: formas sobre la misma retícula de 24×24, sin rellenar para que currentColor les llegue. Llámalo una vez desde un módulo que importen tus páginas:

import { registerIcons } from 'sitelo/ui'

registerIcons({
  logo: '<path d="M4 20 12 4l8 16z"/>',
  // Un nombre que ya existe lo reemplaza en todas partes, y así es como se
  // reestiliza uno de serie sin bifurcar la biblioteca.
  check: '<path d="m5 13 4 4 10-11"/>',
  // Una sola forma cerrada, para que pueda responder a `filled` como los de serie.
  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')                   // tu glifo
icon('check')                  // ahora también tuyo

registerIcons({ check: null }) // y de vuelta al de serie

Por qué en línea y no un sprite

Los iconos se dibujan dentro de la página en vez de sacarse de un icons.svg con <use>. Un sprite ahorra del orden de cien bytes gzipeados de HTML por página y cuesta un viaje de ida y vuelta a cambio: el marcado repetido es justo el caso en el que mejor se porta gzip, así que casi todo lo que un sprite existe para deduplicar ya está deduplicado. En línea significa además que no hay archivo que emitir, ni ruta base que configurar, ni nada que pueda faltar en dist — el mismo trato que hace styles({ inline: true }).

Props

PropTipoPor defectoDescripción
namestring—Qué glifo. Se puede pasar como primer argumento en su lugar.
size'sm' | 'md' | 'lg' | string'md'Un token, o cualquier longitud CSS. Por defecto es 1em.
labelstring—Anunciarlo como imagen con este nombre, en vez de ocultarlo.
spinbooleanfalseRotarlo continuamente.
filledbooleanfalsePintar el glifo en vez de perfilarlo. Los glifos que no se pueden rellenar lo ignoran.

Un nombre desconocido no dibuja nada en vez de lanzar un error: una prop cosmética no debería poder tumbar una compilación. hasIcon(name) te dice si existe alguno, iconNames() los lista todos, y fillableIcons() los que admiten filled.