Ícones

icon() devolve um <svg> inline. Todos os símbolos são desenhados na mesma grelha de 24×24, em traços sem preenchimento a currentColor, por isso herdam a cor e o tamanho de letra daquilo onde estão e não precisam de estilo próprio.

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

Num componente

Um ícone é um filho como qualquer outro. Como se dimensiona em em, combina com a etiqueta ao lado sem que lhe digam o tamanho dela:

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

Tamanho

A predefinição é 1em — o tamanho do texto à volta. size aceita um token ou qualquer comprimento CSS quando queres afastar-te disso:

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

Cor

Não há prop de cor. Um ícone é desenhado a currentColor, por isso toma a cor do seu contexto — é isso que faz um só conjunto funcionar 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' })),
)

Nomes acessíveis

Um ícone leva aria-hidden por predefinição, o que está certo muito mais vezes do que não: um ícone ao lado da palavra «Eliminar» não deve ser anunciado uma segunda vez. Dá-lhe um label só quando for o ícone a carregar todo o significado — aí passa a role="img" com esse nome.

icon('trash')                        // decorativo — escondido
button(icon('trash'), 'Eliminar')    // é a palavra que fala

icon('trash', { label: 'Eliminar' }) // anunciado como imagem

// Um botão só de ícone dá nome ao botão, não ao símbolo lá dentro
iconButton({ label: 'Eliminar', icon: icon('trash') })

Preenchido

filled pinta um símbolo em vez de o contornar. É o mesmo traçado em qualquer dos casos — só muda o atributo fill — por isso as duas formas partilham exatamente a mesma aresta exterior e não podem divergir.

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

E os mesmos nomes, preenchidos:

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

É uma prop e não um segundo conjunto de nomes porque o estado preenchido é quase sempre um estado — guardado, com gosto, avaliado — e por isso quer um booleano, não uma cadeia diferente:

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

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

Os símbolos de estado preenchem-se de outra maneira, porque a marca deles fica dentro da forma. Pintar o círculo engoliria o visto, por isso a marca é recortada dele:

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

Esses trazem um segundo desenho — a forma cheia com a marca recortada por fill-rule: evenodd — porque um recorte assim não se consegue do traçado de contorno mudando um atributo. A forma exterior é desenhada na aresta exterior do contorno, por isso as duas versões acabam na mesma silhueta. É a mesma prop em qualquer dos casos; que mecanismo cada símbolo usa é problema dele.

Um chevron não tem interior nenhum para pintar — é uma linha aberta — por isso preenche até ao triângulo que os seus três pontos descrevem, mantendo o traço que arredonda os cantos:

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 tudo o que responde a filled. Um símbolo sem forma preenchida ignora-o e fica em contorno — preencher eye perderia a pupila e tag o seu furo, por isso nenhum deles finge.

Rodar

spin faz o símbolo rodar — pensado para spinner, embora nada te impeça de rodar refresh enquanto algo recarrega. Com prefers-reduced-motion abranda até quase parar em vez de parar, porque um indicador parado parece avariado.

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

O conjunto

Os nomes descrevem o desenho e não a função que ele cumpre — x-circle, não error — porque o mesmo desenho serve funções sem relação entre si, e um nome que descreve a imagem continua verdadeiro quando isso acontece. Os aliases abaixo cobrem as intenções comuns.

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

Vêm oito marcas com o conjunto — facebook, google, instagram, linkedin, tiktok, whatsapp, x-twitter e youtube. Continuam a aceitar size e label e a desenhar-se a 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'), 'Partilhar'),
)

São reproduções de marcas de outros e não desenhos ao estilo desta biblioteca, por isso quebram de propósito duas das suas regras: são formas cheias em vez de traços, que é o que um logótipo é, e as proporções são as da marca e não as desta grelha. filled não lhes diz nada — já o são.

O desenho vem do Simple Icons, que o publica sob CC0. Isso cobre o desenho, não a marca registada: usa-os para apontar àquilo que nomeiam — uma ligação de perfil, um botão de partilha — e não num produto teu.

É x-twitter, não x, porque x já é alias de close e um botão de fechar a transformar-se num logótipo seria uma surpresa desagradável. twitter também chega lá.

Aliases

Cada um destes desenha um símbolo listado acima, com o nome a que é mais provável que recorras:

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

Os teus próprios ícones

registerIcons() acrescenta um símbolo, ou substitui um que já vem. A marcação é o conteúdo do <svg> — formas na mesma grelha de 24×24, deixadas sem preenchimento para que o currentColor lhes chegue. Chama-o uma vez a partir de um módulo que as tuas páginas importem:

import { registerIcons } from 'sitelo/ui'

registerIcons({
  logo: '<path d="M4 20 12 4l8 16z"/>',
  // Um nome que já existe substitui-o em todo o lado, e é assim que se
  // reestiliza um ícone incluído sem fazer fork da biblioteca.
  check: '<path d="m5 13 4 4 10-11"/>',
  // Uma forma fechada, para poder responder a `filled` como os incluídos.
  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')                   // o teu símbolo
icon('check')                  // agora também teu

registerIcons({ check: null }) // e de volta ao incluído

Porquê inline, e não um sprite

Os ícones são desenhados na página em vez de puxados de um icons.svg com <use>. Um sprite poupa qualquer coisa como uma centena de bytes gzipados de HTML por página e custa uma ida e volta à rede para isso — marcação repetida é justamente o caso em que o gzip é melhor, por isso quase tudo o que um sprite existe para desduplicar já foi desduplicado. Inline significa ainda que não há ficheiro para emitir, nem caminho base para configurar, nem nada que possa faltar em dist — a mesma troca que o styles({ inline: true }) faz.

Props

PropTipoPredefiniçãoDescrição
namestring—Que símbolo. Pode ser passado como primeiro argumento em vez disso.
size'sm' | 'md' | 'lg' | string'md'Um token, ou qualquer comprimento CSS. A predefinição é 1em.
labelstring—Anunciá-lo como imagem com este nome, em vez de o esconder.
spinbooleanfalseFazê-lo rodar continuamente.
filledbooleanfalsePintar o símbolo em vez de o contornar. Ignorado pelos que não podem ser preenchidos.

Um nome desconhecido não desenha nada em vez de lançar um erro — uma prop cosmética não devia poder falhar uma construção. hasIcon(name) diz-te se existe algum, iconNames() lista-os todos, e fillableIcons() dá os que aceitam filled.