Ícones no SvelteKit
O glyphfox/svelte renderiza no servidor, então as páginas do SvelteKit entregam SVG de verdade no HTML, seja a página renderizada a cada requisição ou pré-renderizada no build.
Instalação
npm install glyphfox
Não há nada para configurar. O glyphfox/svelte é exposto pela condição de exportação svelte, então o vite-plugin-svelte, que todo projeto SvelteKit usa, o reconhece como uma biblioteca Svelte e compila os componentes dele para o servidor e para o navegador. Os componentes precisam do Svelte 5; o guia do Svelte detalha os componentes e as props.
Registre os nomes de ícones no layout raiz
Componentes como HomeIcon registram os próprios dados. Nomes que só aparecem em strings, em Icon, Fusion, MorphIcon ou morphTo, precisam de registerIcons, e o melhor lugar para isso é um módulo que o layout raiz importa: ele roda no servidor antes da primeira renderização e no navegador antes da hidratação.
import { registerIcons } from 'glyphfox/svelte'; import menu from 'glyphfox/icons/menu'; import x from 'glyphfox/icons/x'; import moon from 'glyphfox/icons/moon'; import file from 'glyphfox/icons/file'; registerIcons([menu, x, moon, file]);
<script> import '$lib/icons.js'; import { MorphIcon } from 'glyphfox/svelte'; let { children } = $props(); let open = $state(false); </script> <header> <button type="button" aria-label="Menu" aria-expanded={open} onclick={() => (open = !open)}> <MorphIcon name={open ? 'x' : 'menu'} /> </button> </header> {@render children()}
O registro é estado de módulo. No servidor, ele é compartilhado por todas as requisições, o que é seguro porque os dados dos ícones nunca mudam, e é preenchido uma única vez, quando o módulo carrega pela primeira vez.
O que o servidor renderiza
Os componentes de ícone geram o svg completo durante a renderização no servidor. As animações de estado vivo são uma folha de estilo dentro do svg, então um spinner já gira no HTML do servidor, a não ser que o visitante prefira movimento reduzido. Os morphs e os gatilhos de clique e de hover começam depois da hidratação, porque o $effect não roda no servidor.
Para um morph cujo estado final depende de dados, use trigger="state" com active. O servidor renderiza o estado certo do morph, e a animação só toca quando o valor muda no navegador. Com o gatilho padrão (mount), o servidor renderiza o ícone inicial e o morph acontece a cada carregamento de página.
export function load({ cookies }) { return { theme: cookies.get('theme') ?? 'light' }; }
<script> import { SunIcon } from 'glyphfox/svelte'; let { theme } = $props(); let dark = $state(theme === 'dark'); function toggle() { dark = !dark; document.cookie = `theme=${dark ? 'dark' : 'light'}; path=/; max-age=31536000`; } </script> <button type="button" aria-label="Dark theme" aria-pressed={dark} onclick={toggle}> <SunIcon morphTo="moon" trigger="state" active={dark} /> </button>
Ler o tema de um cookie em uma função load do servidor dá ao servidor e ao navegador o mesmo valor inicial. Renderize o componente a partir do layout raiz com <ThemeToggle theme={data.theme} />; moon está registrado em src/lib/icons.js.
Arquivos SVG a partir de um endpoint
O toSvg do ponto de entrada principal glyphfox renderiza qualquer spec em uma string, o que permite criar uma rota +server.js que serve ícones como arquivos: um favicon, uma imagem para um template de e-mail ou uma prévia de link. Com entries, os arquivos são pré-renderizados no build.
import { error } from '@sveltejs/kit'; import { iconNames, toSvg } from 'glyphfox'; export const prerender = true; export function entries() { return ['fox', 'home', 'search'].map((name) => ({ name })); } export function GET({ params }) { if (!iconNames.includes(params.name)) error(404, 'Unknown icon'); const svg = toSvg(params.name, { size: 32, color: '#ff7a1a', variant: 'duotone' }); return new Response(svg, { headers: { 'content-type': 'image/svg+xml' } }); }
<link rel="icon" href="/icons/fox.svg" type="image/svg+xml" />
Um arquivo SVG avulso não tem um elemento pai de quem herdar a cor, então passe color: o valor é gravado no svg e os traços em currentColor passam a usá-lo. Retorne iconNames.map((name) => ({ name })) em entries para pré-renderizar todos os ícones.
O ponto de entrada principal inclui os dados de todos os ícones. Isso não é problema em arquivos +server.js e +page.server.js, que nunca chegam ao navegador; importado em um componente, o toSvg mandaria o conjunto inteiro para o cliente.
Marcação só do servidor com {@html}
Uma função load em +page.server.js pode retornar strings já renderizadas que a página insere com {@html}. O navegador recebe a marcação no HTML e nos dados serializados da página, mas nenhum código de ícone.
import { toSvg } from 'glyphfox'; const features = [ { icon: 'zap', title: 'Fast' }, { icon: 'shield-check', title: 'Safe' }, { icon: 'accessibility', title: 'Accessible' }, ]; export function load() { return { features: features.map((f) => ({ ...f, svg: toSvg(f.icon, { size: 28, weight: 1.5 }) })), }; }
<script> let { data } = $props(); </script> <ul class="features"> {#each data.features as feature (feature.icon)} <li>{@html feature.svg} {feature.title}</li> {/each} </ul>
O {@html} é seguro aqui porque as strings vêm do seu próprio código de servidor, nunca de entrada do usuário. Sem um title, os ícones são renderizados com aria-hidden, o que é o certo ao lado de um rótulo visível.
adapter-static e pré-renderização
Com o adapter-static, ou com export const prerender = true em qualquer rota, as páginas são renderizadas no build e os ícones são gravados nos arquivos HTML. As regras da renderização no servidor também valem no build: um nome que o build não consegue resolver é gravado como um svg vazio.
import adapter from '@sveltejs/adapter-static'; export default { kit: { adapter: adapter() }, };
export const prerender = true;
Para páginas que não precisam de JavaScript, adicione export const csr = false: os ícones continuam como SVG estático e nenhum código de componente é enviado. As animações CSS continuam tocando, enquanto os morphs e os gatilhos de clique ou hover, que precisam de JavaScript, não fazem nada.
Nomes em builds de produção
Em desenvolvimento, o Vite pré-empacota o glyphfox, então qualquer nome parece funcionar. Em um build de produção, o Vite não inclui no bundle o carregador sob demanda que o glyphfox mantém em node_modules, porque ignora imports dinâmicos com variáveis em dependências, e os nomes não registrados ficam vazios. Registre esses nomes ou deixe o Vite incluir o carregador no bundle; ele passa a gerar um chunk pequeno por ícone:
import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], build: { dynamicImportVarsOptions: { exclude: [/node_modules\/(?!glyphfox\/)/] }, }, });
Esses chunks só ajudam no navegador, depois da hidratação. A renderização no servidor não espera por eles, então o registro continua sendo o jeito de colocar um ícone no HTML.
Props
O SvelteKit usa os componentes de glyphfox/svelte, então as props são as compartilhadas, em camelCase.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
size | number, string | 24 | Largura e altura; números são pixels. |
color | string | currentColor | Cor principal. |
secondaryColor | string | color | Cor da camada duotone e do preenchimento sólido. |
secondaryOpacity | 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. |
absoluteWeight | 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. |
flipX | boolean | false | Espelha na horizontal. |
flipY | boolean | false | Espelha na vertical. |
mirrorRtl | 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. |
modifierCorner | top-left, top-right, bottom-left, bottom-right | bottom-right | Canto do selo. |
morphTo | 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. |
Armadilhas comuns
- Registrar no onMount ou em código só do navegador
- Código em
onMount, em$effectou dentro deif (browser)roda depois que o servidor já renderizou. O HTML fica então com ícones vazios que aparecem de repente depois da hidratação. Registre em um módulo que o layout raiz importa. - Um ícone diferente no servidor e no navegador
- O conteúdo do svg é inserido com
{@html}, e o Svelte mantém a marcação do servidor de um bloco{@html}quando a primeira renderização no cliente é diferente (em desenvolvimento, ele avisa comhydration_html_changed). Por isso, um ícone escolhido a partir delocalStorageouwindowpode continuar errado até a próxima mudança. Derive o primeiro valor de dados que o servidor também tem, como um cookie lido em uma função load. - toSvg em um componente compartilhado
- Importar
toSvgdeglyphfoxem um arquivo.sveltepuxa os dados de todos os ícones para o bundle do cliente. Mantenha-o em arquivos de servidor ou use os componentes. - Um morph pré-renderizado no estado errado
- Com o gatilho mount, o HTML pré-renderizado mostra o ícone inicial e o morph acontece depois da hidratação. Use
trigger="state"para tudo o que deve ser pré-renderizado já no estado final.
Perguntas frequentes
Posso pré-renderizar todos os ícones como arquivos SVG?
Sim. Retorne iconNames.map((name) => ({ name })) em entries no endpoint acima, e o build grava um arquivo por ícone em /icons/. Cada arquivo tem algumas centenas de bytes.
Os ícones funcionam em páginas com csr = false?
Sim. O servidor grava o svg no HTML, e as animações de estado vivo continuam tocando porque são CSS dentro do svg. Os morphs e os gatilhos de clique e de hover precisam de JavaScript, então ficam parados no primeiro quadro.