Frameworks

Í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

terminal
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

pages/index.vue
<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.

plugins/glyphfox.ts
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.

components/AppIcon.vue
<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>
components/NotificationsButton.vue
<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.

components/ThemeToggle.vue
<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:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: { componentIslands: true },
});
components/FooterLinks.server.vue
<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:

nuxt.config.ts
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.

PropNos templatesTipoPadrãoDescrição
sizesizenumber, string24Largura e altura; números são pixels.
colorcolorstringcurrentColorCor principal.
secondaryColorsecondary-colorstringcolorCor da camada duotone e do preenchimento sólido.
secondaryOpacitysecondary-opacitynumber0.3 / 1Opacidade da camada duotone ou do preenchimento sólido.
weightweightnumber2Traço de 1 a 3 na grade de 24, com compensação óptica abaixo de 24 px.
absoluteWeightabsolute-weightbooleanfalseMantém o traço com weight pixels em qualquer tamanho.
cornerscornersround, sharproundJunções e pontas redondas ou retas.
variantvariantoutline, duotone, solidoutlineEixo de estilo; o sólido vem das formas fechadas.
rotaterotatenumber0Rotação em graus.
flipXflip-xbooleanfalseEspelha na horizontal.
flipYflip-ybooleanfalseEspelha na vertical.
mirrorRtlmirror-rtlbooleanfalseEspelha em layouts da direita para a esquerda.
animateanimatedraw, pop, wiggle, spin, pulse, bounce, shakenenhumAnimação de estado vivo.
triggertriggermount, hover, click, statemountQuando a animação ou o morph acontece.
activeactivebooleanfalseO estado para o gatilho state.
durationdurationnumber (ms)por animaçãoDuração da animação ou do morph.
delaydelaynumber (ms)0Atraso da animação ou do morph.
looploopboolean, numberfalseRepete para sempre ou um número de vezes (spin e pulse repetem por padrão).
modifiermodifierplus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dotnenhumSelo com espaço de recorte automático.
modifierCornermodifier-cornertop-left, top-right, bottom-left, bottom-rightbottom-rightCanto do selo.
morphTomorph-toname, datanenhumDestino do morph; o gatilho decide quando aparece.
titletitlestringnenhumNome acessível: title, role img e aria-label.
decorativedecorativebooleantrue sem titleEsconde 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/vue ali 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.ts roda 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-to com o gatilho padrão mount anima logo depois da hidratação. Use trigger="state" com um estado vindo de useState, como no botão de troca de tema acima.
Nomes diferentes no servidor e no navegador
Escolher um ícone a partir de window, localStorage ou 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 em onMounted ou 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.

Ícones deste guia