Frameworks

Icônes dans Nuxt

glyphfox/vue prend en charge le rendu côté serveur : les pages Nuxt arrivent donc avec du vrai SVG dans leur HTML. Ce guide présente la mise en place, un plugin pour les icônes partagées, un wrapper auto-importé, ainsi que le comportement de l'enregistrement et des morphs entre le serveur et le navigateur.

Installation

terminal
npm install glyphfox

Il n'y a aucun module Nuxt à ajouter. Les composants Vue se trouvent dans glyphfox/vue, distribué sous forme de modules ES standards avec un build CommonJS pour require : Nuxt l'importe donc sur le serveur et dans le navigateur comme n'importe quelle autre dépendance. Tout le contenu du guide Vue s'applique ; cette page couvre ce qui change lorsque ces mêmes composants sont d'abord rendus sur le serveur.

Utiliser les icônes dans les pages et les composants

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>

Le svg fait partie du HTML rendu par le serveur : les icônes sont visibles avant l'exécution du moindre JavaScript et ne décalent jamais la mise en page au moment de l'hydratation. Après l'hydratation, ces mêmes composants gèrent les déclencheurs, les morphs et les changements d'état. mirror-rtl affiche en miroir les icônes directionnelles, comme les flèches, lorsque la page ou un parent porte dir="rtl".

Importer les composants dans chaque fichier est la configuration la plus explicite, et celle qui garde léger le chunk de chaque route : une page n'embarque que les icônes qu'elle utilise.

Un plugin pour les icônes partagées

Un plugin est l'endroit tout indiqué pour deux choses : les composants qui doivent être disponibles dans tous les templates, et les données des noms qui n'apparaissent que sous forme de chaînes, dans 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);
});

Gardez ce plugin universel, sans suffixe .client ni .server : le serveur a besoin des données pour écrire le HTML, et le navigateur a besoin des mêmes données pour l'hydrater. Sur le serveur, le registre est un état de module partagé par toutes les requêtes, ce qui ne pose aucun problème puisque les données des icônes ne changent jamais.

Un wrapper auto-importé

Nuxt importe automatiquement les composants de components/. Un petit wrapper fournit à l'application un composant d'icône unique, avec vos valeurs par défaut et une liste fixe de noms : une faute de frappe fait ainsi échouer la vérification des types au lieu de produire un svg vide.

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>

Passer les données de l'icône par la prop icon contourne entièrement le registre des noms : le wrapper rend donc le même balisage sur le serveur et dans le navigateur, quel que soit ce qui a été enregistré et où. Sans label, l'icône reste décorative, ce qui est le bon choix dans un bouton qui possède déjà un libellé.

Morphs, animations et hydratation

Le serveur rend une image fixe. Les animations des états vivants sont du CSS intégré au svg : un spinner avec animate="spin" tourne donc déjà dans le HTML du serveur, sauf si le visiteur a demandé un mouvement réduit. Les morphs et les déclencheurs de survol et de clic nécessitent d'abord l'hydratation du composant.

morph-to avec le déclencheur par défaut (mount) rend l'icône de départ sur le serveur et joue le morph juste après l'hydratation, ce qui ressemble à un scintillement à chaque chargement de page. Lorsque la page doit s'ouvrir dans l'état cible, utilisez trigger="state" avec :active lié à un état partagé : le serveur rend la cible, et le morph ne se joue que lorsque l'état change.

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>

useState est sérialisé dans le payload de la page : le serveur et le navigateur partent ainsi de la même valeur, et l'icône s'hydrate sans incohérence. La cible du morph, moon, est enregistrée dans le plugin ci-dessus.

Bundle et payload

Dans une application rendue côté serveur, une icône coûte deux fois : le balisage svg dans le HTML, et le code du composant qui l'hydrate. Le balisage est léger, quelques tracés par icône. Mesuré sur un build de production Vite 8, le runtime Vue partagé de glyphfox pèse environ 20 Ko gzip et chaque composant d'icône ajoute à peu près 0,1 Ko gzip : le runtime domine, et les icônes supplémentaires coûtent peu.

Ne placez pas de données d'icônes dans useState ni dans useAsyncData. Tout ce qui y est stocké est sérialisé dans le HTML de chaque page qui l'utilise. Importez plutôt les modules d'icônes : ils sont mis en cache avec le reste de votre JavaScript.

Pour les icônes qui ne changent jamais, par exemple dans un pied de page ou une section marketing statique, un composant serveur rend le balisage sans envoyer le moindre code d'icône. Activez les îles de composants (component islands) et nommez le composant avec le suffixe .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>

Les noms non enregistrés

Un nom que personne n'a enregistré est récupéré par un import dynamique à l'intérieur du paquet. Nuxt utilise Vite pour le build, et Vite ignore les imports dynamiques contenant des variables lorsqu'ils se trouvent dans node_modules : dans un build de production, ces noms ne se chargent donc jamais. Si vous avez vraiment besoin de n'importe quel nom à l'exécution, par exemple dans un sélecteur d'icônes, laissez Vite intégrer le chargeur au bundle. Il génère alors un petit chunk par icône, récupéré uniquement lorsque ce nom est utilisé :

nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    build: {
      dynamicImportVarsOptions: { exclude: [/node_modules\/(?!glyphfox\/)/] },
    },
  },
});

Cela n'aide qu'après l'hydratation. Le rendu côté serveur n'attend pas un import dynamique : une icône qui doit figurer dans le HTML doit donc toujours être enregistrée ou importée en tant que composant.

Props

Les composants sont ceux de glyphfox/vue, avec les mêmes props. Dans les templates, vous pouvez utiliser les noms d'attributs en kebab-case.

PropDans les templatesTypePar défautDescription
sizesizenumber, string24Largeur et hauteur ; les nombres sont en pixels.
colorcolorstringcurrentColorCouleur principale.
secondaryColorsecondary-colorstringcolorCouleur du calque duotone et du remplissage plein.
secondaryOpacitysecondary-opacitynumber0.3 / 1Opacité du calque duotone ou du remplissage plein.
weightweightnumber2Trait de 1 à 3 sur la grille de 24, avec compensation optique en dessous de 24 px.
absoluteWeightabsolute-weightbooleanfalseGarde le trait à weight pixels quelle que soit la taille.
cornerscornersround, sharproundJointures et extrémités arrondies ou vives.
variantvariantoutline, duotone, solidoutlineAxe de style ; solid vient des formes fermées.
rotaterotatenumber0Rotation en degrés.
flipXflip-xbooleanfalseMiroir horizontal.
flipYflip-ybooleanfalseMiroir vertical.
mirrorRtlmirror-rtlbooleanfalseMiroir dans les mises en page de droite à gauche.
animateanimatedraw, pop, wiggle, spin, pulse, bounce, shakeaucunPréréglage d'état vivant.
triggertriggermount, hover, click, statemountMoment où l'animation ou le morph se joue.
activeactivebooleanfalseL'état pour le déclencheur state.
durationdurationnumber (ms)selon le préréglageDurée de l'animation ou du morph.
delaydelaynumber (ms)0Délai de l'animation ou du morph.
looploopboolean, numberfalseRépète à l'infini ou un certain nombre de fois (spin et pulse bouclent par défaut).
modifiermodifierplus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dotaucunBadge avec espace de découpe automatique.
modifierCornermodifier-cornertop-left, top-right, bottom-left, bottom-rightbottom-rightCoin du badge.
morphTomorph-toname, dataaucunCible du morph ; le déclencheur décide quand elle s'affiche.
titletitlestringaucunNom accessible : title, role img et aria-label.
decorativedecorativebooleantrue sans titleMasque l'icône aux technologies d'assistance.

Pièges courants

Fonctionne en développement, vide en production
Le serveur de développement pré-bundle l'ensemble du paquet, si bien que chaque nom est résolu localement. Le build de production ne conserve que vos imports et laisse de côté le chargeur à la demande : les noms utilisés uniquement dans des chaînes rendent alors un svg vide. Enregistrez-les dans le plugin universel.
Une icône s'affiche côté serveur, puis disparaît
Lorsque le serveur charge glyphfox sans tree shaking, y importer glyphfox/vue enregistre toutes les icônes, alors que le bundle du navigateur n'enregistre que ce que vous importez. Un nom utilisé uniquement sous forme de chaîne peut alors apparaître dans le HTML du serveur, puis devenir vide après l'hydratation, avec un avertissement d'hydratation dans la console. Enregistrer explicitement chacun de ces noms supprime cette différence.
Enregistrer dans un plugin réservé au client
Un fichier plugins/glyphfox.client.ts s'exécute après que le serveur a déjà rendu la page : le HTML contient donc des icônes vides qui apparaissent brusquement plus tard. Utilisez un plugin sans suffixe.
Un morph qui se joue à chaque chargement de page
morph-to avec le déclencheur mount par défaut s'anime juste après l'hydratation. Utilisez trigger="state" avec un état issu de useState, comme dans le bouton de changement de thème ci-dessus.
Des noms différents sur le serveur et dans le navigateur
Choisir une icône à partir de window, de localStorage ou de l'heure actuelle donne au navigateur un premier rendu différent de celui du serveur, et Vue signale une incohérence d'hydratation. Lisez ces valeurs dans onMounted, ou stockez-les dans un cookie que le serveur peut lire.

Questions fréquentes

Ai-je besoin d'un module Nuxt pour glyphfox ?

Non. Importez les composants depuis glyphfox/vue là où vous les utilisez, ou enregistrez un ensemble partagé dans un plugin. Un module n'apporterait que l'enregistrement global, qu'un plugin de dix lignes vous offre déjà, sans masquer les icônes qui se retrouvent dans le bundle.

Est-ce compatible avec nuxt generate et les routes prérendues ?

Oui. Le prérendu exécute le même rendu côté serveur au moment du build : les fichiers HTML générés contiennent donc le balisage svg. La règle d'enregistrement est la même : un nom que le build ne peut pas résoudre est écrit sous forme de svg vide.

Icônes de ce guide