Ícones no Nuxt
O glyphfox/vue renderiza no servidor, então as páginas do Nuxt chegam com SVG de verdade no HTML. Este guia mostra a configuração, um plugin para ícones compartilhados, um wrapper importado automaticamente e como o registro e os morphs se comportam entre o servidor e o navegador.
Instalação
npm install glyphfox
Não há nenhum módulo do Nuxt para adicionar. Os componentes Vue ficam em glyphfox/vue, que é publicado como módulos ES puros, com um build CommonJS para require, então o Nuxt o importa no servidor e no navegador como qualquer outra dependência. Tudo o que está no guia do Vue vale aqui; esta página cobre o que muda quando os mesmos componentes renderizam primeiro no servidor.
Use ícones em páginas e componentes
<script setup lang="ts"> import { SearchIcon, ArrowRightIcon } from 'glyphfox/vue'; </script> <template> <form class="search" action="/search"> <SearchIcon :size="18" /> <input name="q" type="search" placeholder="Search the docs" aria-label="Search the docs"> <button type="submit"> Search <ArrowRightIcon :size="16" mirror-rtl /> </button> </form> </template>
O svg faz parte do HTML do servidor, então os ícones ficam visíveis antes de qualquer JavaScript rodar e nunca deslocam o layout quando a página é hidratada. Depois da hidratação, os mesmos componentes cuidam dos gatilhos, dos morphs e das mudanças de estado. mirror-rtl espelha ícones direcionais, como setas, quando a página ou um elemento pai tem dir="rtl".
Importar os componentes em cada arquivo é a configuração mais explícita e a que mantém pequeno o chunk de cada rota: uma página só carrega os ícones que usa.
Um plugin para ícones compartilhados
Um plugin é o lugar para duas coisas: componentes que todo template deve enxergar e os dados de nomes que só aparecem como strings, em Icon, Fusion, MorphIcon ou morph-to.
import { HomeIcon, SettingsIcon, Icon, registerIcons } from 'glyphfox/vue'; import menu from 'glyphfox/icons/menu'; import x from 'glyphfox/icons/x'; import moon from 'glyphfox/icons/moon'; import cloud from 'glyphfox/icons/cloud'; import lock from 'glyphfox/icons/lock'; // Runs once per process on the server and once in the browser. registerIcons([menu, x, moon, cloud, lock]); export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.component('HomeIcon', HomeIcon); nuxtApp.vueApp.component('SettingsIcon', SettingsIcon); nuxtApp.vueApp.component('GfIcon', Icon); });
Mantenha esse plugin universal, sem o sufixo .client ou .server: o servidor precisa dos dados para escrever o HTML, e o navegador precisa dos mesmos dados para hidratá-lo. No servidor, o registro é um estado de módulo compartilhado por todas as requisições, o que é seguro porque os dados dos ícones nunca mudam.
Um wrapper com importação automática
O Nuxt importa automaticamente os componentes de components/. Um wrapper pequeno dá ao app um único componente de ícone com os seus valores padrão e uma lista fixa de nomes, então um erro de digitação falha na checagem de tipos em vez de renderizar um svg vazio.
<script setup lang="ts"> import { Icon } from 'glyphfox/vue'; import home from 'glyphfox/icons/home'; import search from 'glyphfox/icons/search'; import settings from 'glyphfox/icons/settings'; import bell from 'glyphfox/icons/bell'; const icons = { home, search, settings, bell }; const props = withDefaults(defineProps<{ name: keyof typeof icons; label?: string; size?: number }>(), { size: 20 }); </script> <template> <Icon :icon="icons[props.name]" :size="props.size" :weight="1.75" :title="props.label" /> </template>
<template> <button type="button" aria-label="Notifications"> <AppIcon name="bell" /> </button> </template>
Passar os dados do ícone pela prop icon ignora completamente o registro de nomes, então o wrapper renderiza a mesma marcação no servidor e no navegador, não importa o que foi registrado e onde. Sem um label, o ícone fica decorativo, o que é o certo dentro de um botão com rótulo.
Morphs, animações e hidratação
O servidor renderiza um quadro parado. As animações de estado vivo são CSS dentro do svg, então um spinner com animate="spin" já gira no HTML do servidor, a não ser que o visitante prefira movimento reduzido. Os morphs e os gatilhos de hover e de clique precisam que o componente seja hidratado antes.
morph-to com o gatilho padrão (mount) renderiza o ícone inicial no servidor e executa o morph logo depois da hidratação, o que parece uma piscada a cada carregamento de página. Quando a página precisar abrir já no estado de destino, use trigger="state" com :active ligado a um estado compartilhado: o servidor renderiza o destino, e o morph só acontece quando o estado muda.
<script setup lang="ts"> import { SunIcon } from 'glyphfox/vue'; const dark = useState('dark', () => false); </script> <template> <button type="button" aria-label="Dark theme" :aria-pressed="dark" @click="dark = !dark"> <SunIcon morph-to="moon" trigger="state" :active="dark" /> </button> </template>
O useState é serializado no payload da página, então o servidor e o navegador começam do mesmo valor e o ícone é hidratado sem incompatibilidade. O destino do morph, moon, está registrado no plugin acima.
Bundle e payload
Em um app renderizado no servidor, um ícone custa duas vezes: a marcação do svg no HTML e o código do componente que o hidrata. A marcação é pequena, alguns paths por ícone. Em um build de produção com Vite 8, o runtime Vue compartilhado do glyphfox tem cerca de 20 KB gzip e cada componente de ícone acrescenta uns 0,1 KB gzip, então o runtime domina e ícones a mais saem baratos.
Mantenha os dados dos ícones fora de useState e useAsyncData. Tudo o que fica guardado ali é serializado no HTML de cada página que usa esses dados. Em vez disso, importe os módulos dos ícones; eles ficam em cache junto com o resto do seu JavaScript.
Para ícones que nunca mudam, como os de um rodapé ou de uma seção de marketing estática, um componente de servidor renderiza a marcação sem enviar nenhum código de ícone. Ative as ilhas de componentes (component islands) e dê ao componente o sufixo .server.vue:
export default defineNuxtConfig({ experimental: { componentIslands: true }, });
<script setup lang="ts"> import { GitBranchIcon, RssIcon, MailIcon } from 'glyphfox/vue'; </script> <template> <ul class="footer-links"> <li><a href="https://github.com/kwy404/glyphfox"><GitBranchIcon :size="18" /> Source</a></li> <li><a href="/feed.xml"><RssIcon :size="18" /> Feed</a></li> <li><a href="mailto:hello@example.com"><MailIcon :size="18" /> Contact</a></li> </ul> </template>
Nomes que não foram registrados
Um nome que ninguém registrou é buscado por um import dinâmico dentro do pacote. O Nuxt faz o build com Vite, e o Vite ignora imports dinâmicos com variáveis quando eles estão em node_modules, então, em um build de produção, esses nomes nunca carregam. Se você realmente precisa de qualquer nome em tempo de execução, por exemplo em um seletor de ícones, deixe o Vite incluir o carregador no bundle. Ele passa então a gerar um chunk pequeno por ícone, baixado só quando aquele nome é usado:
export default defineNuxtConfig({ vite: { build: { dynamicImportVarsOptions: { exclude: [/node_modules\/(?!glyphfox\/)/] }, }, }, });
Isso só ajuda depois da hidratação. A renderização no servidor não espera um import dinâmico, então um ícone que precisa estar no HTML ainda tem que ser registrado ou importado como componente.
Props
Os componentes são os de glyphfox/vue, com as mesmas props. Nos templates, você pode usar os nomes de atributo em kebab-case.
| Prop | Nos templates | Tipo | Padrão | Descrição |
|---|---|---|---|---|
size | size | number, string | 24 | Largura e altura; números são pixels. |
color | color | string | currentColor | Cor principal. |
secondaryColor | secondary-color | string | color | Cor da camada duotone e do preenchimento sólido. |
secondaryOpacity | secondary-opacity | number | 0.3 / 1 | Opacidade da camada duotone ou do preenchimento sólido. |
weight | weight | number | 2 | Traço de 1 a 3 na grade de 24, com compensação óptica abaixo de 24 px. |
absoluteWeight | absolute-weight | boolean | false | Mantém o traço com weight pixels em qualquer tamanho. |
corners | corners | round, sharp | round | Junções e pontas redondas ou retas. |
variant | variant | outline, duotone, solid | outline | Eixo de estilo; o sólido vem das formas fechadas. |
rotate | rotate | number | 0 | Rotação em graus. |
flipX | flip-x | boolean | false | Espelha na horizontal. |
flipY | flip-y | boolean | false | Espelha na vertical. |
mirrorRtl | mirror-rtl | boolean | false | Espelha em layouts da direita para a esquerda. |
animate | animate | draw, pop, wiggle, spin, pulse, bounce, shake | nenhum | Animação de estado vivo. |
trigger | trigger | mount, hover, click, state | mount | Quando a animação ou o morph acontece. |
active | active | boolean | false | O estado para o gatilho state. |
duration | duration | number (ms) | por animação | Duração da animação ou do morph. |
delay | delay | number (ms) | 0 | Atraso da animação ou do morph. |
loop | loop | boolean, number | false | Repete para sempre ou um número de vezes (spin e pulse repetem por padrão). |
modifier | modifier | plus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dot | nenhum | Selo com espaço de recorte automático. |
modifierCorner | modifier-corner | top-left, top-right, bottom-left, bottom-right | bottom-right | Canto do selo. |
morphTo | morph-to | name, data | nenhum | Destino do morph; o gatilho decide quando aparece. |
title | title | string | nenhum | Nome acessível: title, role img e aria-label. |
decorative | decorative | boolean | true sem title | Esconde o ícone das tecnologias assistivas. |
Armadilhas comuns
- Funciona em desenvolvimento, vazio em produção
- O servidor de desenvolvimento pré-empacota o glyphfox inteiro, então todo nome é resolvido localmente. O build de produção mantém só os seus imports e deixa de fora o carregador sob demanda, então nomes usados só em strings renderizam um svg vazio. Registre esses nomes no plugin universal.
- Um ícone renderiza no servidor e depois some
- Quando o servidor carrega o glyphfox sem tree shaking, importar
glyphfox/vueali registra todos os ícones, enquanto o bundle do navegador registra só o que você importa. Um nome usado só como string pode então aparecer no HTML do servidor e ficar vazio depois da hidratação, com um aviso de hidratação no console. Registrar explicitamente cada um desses nomes elimina a diferença. - Registrar em um plugin só de cliente
- Um
plugins/glyphfox.client.tsroda depois que o servidor já renderizou a página, então o HTML contém ícones vazios que aparecem de repente mais tarde. Use um plugin sem sufixo. - Um morph que acontece a cada carregamento de página
morph-tocom o gatilho padrão mount anima logo depois da hidratação. Usetrigger="state"com um estado vindo deuseState, como no botão de troca de tema acima.- Nomes diferentes no servidor e no navegador
- Escolher um ícone a partir de
window,localStorageou da hora atual faz a primeira renderização no navegador ser diferente da do servidor, e o Vue acusa uma incompatibilidade de hidratação. Leia esses valores emonMountedou guarde-os em um cookie que o servidor consiga ler.
Perguntas frequentes
Preciso de um módulo do Nuxt para o glyphfox?
Não. Importe os componentes de glyphfox/vue onde você os usa ou registre um conjunto compartilhado em um plugin. Um módulo só acrescentaria o registro global, que um plugin de dez linhas já oferece sem esconder quais ícones vão parar no bundle.
Funciona com nuxt generate e rotas pré-renderizadas?
Sim. A pré-renderização executa a mesma renderização no servidor durante o build, então os arquivos HTML gerados contêm a marcação do svg. A regra do registro é a mesma: um nome que o build não consegue resolver é gravado como um svg vazio.