Ícones em Lit e web components
O glyphfox traz um custom element, <glyph-fox>, que recebe o nome do ícone e todas as props compartilhadas como atributos. Ele funciona em qualquer lugar onde HTML funciona e se encaixa nos templates do Lit sem wrapper.
Instalação
Instale o pacote e registre o elemento uma única vez, no módulo de entrada da sua aplicação. Importar glyphfox/web registra todos os ícones e define <glyph-fox> como efeito colateral, então não é preciso mais nada.
npm install glyphfox
import 'glyphfox/web';
Para incluir no bundle só os ícones que você usa, pegue o elemento de glyphfox/web/element, registre os dados dos ícones a partir de glyphfox/icons/NAME e defina o elemento você mesmo. registerIcons vem de glyphfox/core, que compartilha o registro com o elemento.
import { defineGlyphFox } from 'glyphfox/web/element'; import { registerIcons } from 'glyphfox/core'; import menu from 'glyphfox/icons/menu'; import x from 'glyphfox/icons/x'; import bell from 'glyphfox/icons/bell'; import search from 'glyphfox/icons/search'; registerIcons([menu, x, bell, search]); defineGlyphFox();
| Ponto de entrada | O que é entregue | Tamanho (esbuild, minificado) |
|---|---|---|
glyphfox/web | O elemento, o motor e todos os ícones, registrados na importação. | cerca de 209 KB, 59 KB com gzip |
glyphfox/web/element | O elemento e o motor; você registra os ícones. | cerca de 77 KB, 28 KB com gzip, incluindo um ícone, mais algumas centenas de bytes por ícone |
Um nome que você não registrou é carregado sob demanda. O webpack e o esbuild transformam esses carregamentos em um pequeno chunk por ícone, mas o build de produção do Vite deixa o import dentro de node_modules do jeito que foi escrito, e o ícone fica vazio (veja o guia de tree shaking). Registre todos os ícones que você usa com glyphfox/web/element; assim eles também são desenhados já no primeiro frame, sem nenhuma requisição de rede.
Use o elemento
O elemento renderiza um SVG inline dentro do seu shadow root. name aceita a sintaxe de nomes completa, então selos modificadores, fusões e estilos não precisam de nenhuma API extra, e toda prop compartilhada é um atributo em kebab-case.
<glyph-fox name="home"></glyph-fox> <glyph-fox name="file+check" size="32" weight="1.5"></glyph-fox> <glyph-fox name="cloud&lock" layout="overlap"></glyph-fox> <glyph-fox name="heart/solid" color="#e11d48"></glyph-fox> <glyph-fox name="bell" animate="shake" trigger="hover"></glyph-fox>
O elemento observa os próprios atributos. Mude um deles com setAttribute, ou por qualquer template que escreva atributos, e o ícone é redesenhado com o novo valor.
Templates do Lit
No Lit, passe os valores com bindings de atributo. O elemento lê atributos e não tem propriedades JavaScript correspondentes, então um binding de propriedade como .name=${...} define um campo que o elemento nunca lê, e o ícone continua como estava.
import { LitElement, html, css } from 'lit'; import 'glyphfox/web'; export class AppToolbar extends LitElement { static properties = { open: { type: Boolean }, unread: { type: Number }, }; static styles = css` button { display: inline-flex; align-items: center; gap: 8px; color: inherit; } glyph-fox { color: var(--toolbar-icon, currentColor); } `; constructor() { super(); this.open = false; this.unread = 0; } render() { return html` <button type="button" aria-expanded=${this.open} @click=${() => (this.open = !this.open)}> <glyph-fox name=${this.open ? 'x' : 'menu'} morph duration="300"></glyph-fox> <span>Menu</span> </button> <glyph-fox name="bell" animate="shake" trigger="state" ?active=${this.unread > 0} title=${this.unread ? `${this.unread} new notifications` : 'No new notifications'} ></glyph-fox> `; } } customElements.define('app-toolbar', AppToolbar);
Use bindings booleanos para flags: ?morph=${...} e ?active=${...}. O elemento lê um atributo vazio ou "true" como verdadeiro, e "false" ou um atributo ausente como falso, então o binding booleano tem correspondência exata.
Para valores opcionais, faça o binding de nothing (ou use a diretiva ifDefined) para que o atributo seja removido quando não houver valor. Um atributo size vazio deixa o svg interno com largura e altura vazias, e o navegador recorre ao tamanho padrão de svg.
import { html, nothing } from 'lit'; export const iconButton = ({ name, size, label, onClick }) => html` <button type="button" aria-label=${label} @click=${onClick}> <glyph-fox name=${name} size=${size ?? nothing}></glyph-fox> </button> `;
Estilização através do shadow root
O svg fica no shadow root do elemento, com os traços em currentColor. A cor é herdada através do limite do shadow DOM, então defina color no elemento ou em qualquer ancestral. O host usa display: inline-flex com line-height: 0 e vertical-align: middle, então se acomoda numa linha de texto sem CSS extra.
O tamanho vem do atributo size, que define a largura e a altura do svg interno. As propriedades CSS width e height no host não redimensionam o desenho. Use size="1em" para acompanhar o tamanho da fonte, ou preencha o atributo a partir dos seus próprios design tokens.
<p class="note"> <glyph-fox name="circle-info" size="1em" weight="2.5"></glyph-fox> The icon follows the font size of this paragraph. </p> <glyph-fox name="folder" variant="duotone" color="#0f172a" secondary-color="#ff7a1a"></glyph-fox>
.toolbar glyph-fox { color: #6b7280; } .toolbar button:hover glyph-fox { color: #111827; }
Não há parts nem slots para estilizar: tudo o que fica lá dentro é gerado a partir das props. Mude o desenho com atributos como weight, corners, variant e secondary-color, e não com seletores que tentam alcançar o interior do shadow root.
Um nome de tag próprio
defineGlyphFox(tag) define o elemento com outro nome e retorna a classe dele. Use para seguir o prefixo do seu design system ou para evitar conflito com outro elemento. É seguro chamar mais de uma vez: uma tag que já está definida é retornada como está.
import { defineGlyphFox } from 'glyphfox/web/element'; import { registerIcons } from 'glyphfox/core'; import home from 'glyphfox/icons/home'; import settings from 'glyphfox/icons/settings'; registerIcons([home, settings]); defineGlyphFox('acme-icon');
<acme-icon name="home" size="20"></acme-icon> <acme-icon name="settings" size="20" weight="1.5"></acme-icon>
Importar glyphfox/web também define glyph-fox, então escolha um ponto de entrada por aplicação: glyphfox/web para a tag padrão com todos os ícones, glyphfox/web/element quando você escolhe a tag e a lista de ícones.
Gatilhos e morphs
O elemento cuida da própria interação. Com trigger="hover" ou trigger="click", ele escuta os eventos em si mesmo, e com trigger="state", segue o atributo active. O atributo morph anima toda troca de name, enquanto morph-to faz o ícone se transformar em um segundo ícone sempre que o gatilho dispara.
<glyph-fox name="menu" morph-to="x" trigger="click"></glyph-fox> <glyph-fox name="sparkles" animate="draw" trigger="hover"></glyph-fox> <glyph-fox id="visibility" name="eye" morph></glyph-fox> <script type="module"> const eye = document.getElementById('visibility'); eye.addEventListener('click', () => { eye.setAttribute('name', eye.getAttribute('name') === 'eye' ? 'eye-off' : 'eye'); }); </script>
Hover e clique são detectados no próprio elemento. Os estilos da animação ficam dentro do shadow root, onde não enxergam o pai, e o listener de clique fica no host. Quando o botão inteiro deve reagir, controle o ícone a partir do botão: use trigger="state" e defina active nos handlers do botão.
<button type="button" class="save"> <glyph-fox name="save" animate="pop" trigger="state"></glyph-fox> Save </button> <script type="module"> const button = document.querySelector('.save'); const icon = button.querySelector('glyph-fox'); button.addEventListener('pointerenter', () => icon.setAttribute('active', '')); button.addEventListener('pointerleave', () => icon.removeAttribute('active')); </script>
Morphs e presets respeitam prefers-reduced-motion: as transições pulam direto para o fim e as animações não tocam. Os presets e os gatilhos estão descritos em ícones animados.
SVG estático em templates do Lit
Quando um ícone nunca muda depois da renderização, você pode dispensar o elemento e inserir a marcação SVG diretamente. renderToString de glyphfox/core renderiza os dados do ícone com as props compartilhadas, e o Lit insere a string com a diretiva unsafeHTML. O renderizador escapa títulos e valores de atributos, então a marcação pode ser inserida com segurança.
import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { renderToString } from 'glyphfox/core'; import circleCheck from 'glyphfox/icons/circle-check'; const check = unsafeHTML(renderToString(circleCheck, { size: 16, color: '#16a34a' })); export const savedBadge = () => html`<span class="badge">${check} Saved</span>`;
Para a sintaxe de nomes (file+check@tr, cloud&lock), use toSvg de glyphfox. Ele resolve qualquer nome, o que significa que carrega todos os ícones; veja tree shaking antes de usá-lo em um bundle do cliente.
Atributos
Toda prop compartilhada é um atributo em kebab-case. O elemento acrescenta quatro próprios: name para o ícone, com a sintaxe de nomes completa; layout e gap para fusões; e morph, que anima as trocas de name. Números são lidos a partir do texto, e booleanos seguem a regra acima.
| Atributo | Tipo | Padrão | Descrição |
|---|---|---|---|
name | string | nenhum | A especificação do ícone: um nome, name+modifier@corner, a&b:layout ou name/variant. |
layout | overlap, side, stack, grid, orbit, badge, inside | nenhum | Layout da fusão quando o nome junta ícones com &. |
gap | number | 1 | Espaço de recorte de uma fusão, em unidades da grade. |
morph | boolean | false | Anima toda troca de name como um morph. |
size | number, string | 24 | Largura e altura; números são pixels. |
color | string | currentColor | Cor principal. |
secondary-color | string | color | Cor da camada duotone e do preenchimento sólido. |
secondary-opacity | number | 0.3 / 1 | Opacidade da camada duotone ou do preenchimento sólido. |
weight | number | 2 | Traço de 1 a 3 na grade de 24, com compensação óptica abaixo de 24 px. |
absolute-weight | boolean | false | Mantém o traço com weight pixels em qualquer tamanho. |
corners | round, sharp | round | Junções e pontas redondas ou retas. |
variant | outline, duotone, solid | outline | Eixo de estilo; o sólido vem das formas fechadas. |
rotate | number | 0 | Rotação em graus. |
flip-x | boolean | false | Espelha na horizontal. |
flip-y | boolean | false | Espelha na vertical. |
mirror-rtl | boolean | false | Espelha em layouts da direita para a esquerda. |
animate | draw, pop, wiggle, spin, pulse, bounce, shake | nenhum | Animação de estado vivo. |
trigger | mount, hover, click, state | mount | Quando a animação ou o morph acontece. |
active | boolean | false | O estado para o gatilho state. |
duration | number (ms) | por animação | Duração da animação ou do morph. |
delay | number (ms) | 0 | Atraso da animação ou do morph. |
loop | boolean, number | false | Repete para sempre ou um número de vezes (spin e pulse repetem por padrão). |
modifier | plus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dot | nenhum | Selo com espaço de recorte automático. |
modifier-corner | top-left, top-right, bottom-left, bottom-right | bottom-right | Canto do selo. |
morph-to | name, data | nenhum | Destino do morph; o gatilho decide quando aparece. |
title | string | nenhum | Nome acessível: title, role img e aria-label. |
decorative | boolean | true sem title | Esconde o ícone das tecnologias assistivas. |
Acessibilidade
Sem rótulo, o ícone é decorativo: o svg interno recebe aria-hidden="true". Isso é o correto para um ícone ao lado de texto visível e para um botão de ícone que tem o próprio aria-label.
Para dar um nome ao próprio ícone, use o atributo title. Ele adiciona um <title>, role="img" e aria-label ao svg interno. Não conte com aria-label no elemento: o rótulo fica no host, que não tem role, enquanto o svg de dentro vira uma imagem sem nome. title no host também é o atributo global do HTML, então os navegadores o mostram como tooltip.
<!-- decorative: the button carries the name --> <button type="button" aria-label="Close dialog"> <glyph-fox name="x"></glyph-fox> </button> <!-- meaningful on its own --> <glyph-fox name="triangle-alert" title="Warning" color="#b45309"></glyph-fox>
Padrões para links, ícones de status e contraste estão no guia de acessibilidade.
Renderização no servidor e deslocamento de layout
O elemento desenha o ícone quando passa pelo upgrade no navegador. O HTML renderizado no servidor contém só a tag vazia, e o elemento não usa declarative shadow DOM, então o ícone aparece depois que o script roda. Reserve o espaço para que o layout não se mova quando isso acontecer.
glyph-fox:not(:defined) { display: inline-block; width: 24px; height: 24px; vertical-align: middle; } glyph-fox[size="16"]:not(:defined) { width: 16px; height: 16px; }
Quando um ícone precisa estar na primeira pintura, em um e-mail ou no HTML para crawlers, renderize a string SVG no servidor em vez disso. As opções para cada framework estão em renderização no servidor.
Armadilhas comuns
- Tags autofechadas
- Escreva
<glyph-fox name="home"></glyph-fox>. O HTML ignora a barra em<glyph-fox />, então o elemento continua aberto e o conteúdo seguinte vira filho dele, e o shadow root não exibe esses filhos. - Bindings de propriedade
.name=${...}no Lit e outros bindings de propriedade definem um campo JavaScript que o elemento não lê. Faça binding de atributos. O Solid precisa deattr:pelo mesmo motivo, veja o guia de Solid.- Atributos vazios
- Um
sizevazio renderiza um svg com largura e altura vazias. Remova o atributo quando não houver valor, comnothingouifDefinedno Lit. - Hover e clique no elemento pai
- Os gatilhos de hover e clique disparam só no ícone, não no botão em volta dele. Use
trigger="state"e definaactivea partir do pai. - Mudanças de atributo repetem as animações de montagem
- O elemento troca o svg interno sempre que a marcação muda, então, com um preset de
animatee otrigger="mount"padrão, mudarcolorousizetoca a animação de novo. Usetrigger="state"em ícones cujos outros atributos mudam com frequência. - Um ícone vazio
- Um nome desconhecido não renderiza nada e registra um aviso
glyphfox:no console. Um nome válido que não foi registrado é desenhado assim que o módulo dele carrega, então confira o painel de rede antes de procurar um erro de digitação.
Perguntas frequentes
Posso usar o elemento em React, Vue ou Angular?
Sim, é um custom element padrão. Mas, nesses casos, os pacotes de cada framework se encaixam melhor: glyphfox/react, glyphfox/vue e glyphfox-angular oferecem props tipadas e renderizam no servidor. Deixe o elemento para Lit, HTML puro e páginas que misturam stacks. Em templates Vue, informe ao compilador, com compilerOptions.isCustomElement, que glyph-fox é um custom element; caso contrário, o Vue tenta resolvê-lo como componente.