Frameworks

Icônes dans SvelteKit

glyphfox/svelte prend en charge le rendu côté serveur : les pages SvelteKit livrent donc du vrai SVG dans leur HTML, qu'elles soient rendues à chaque requête ou prérendues au moment du build.

Installation

terminal
npm install glyphfox

Il n'y a rien à configurer. glyphfox/svelte est exposé sous la condition d'export svelte : vite-plugin-svelte, qu'utilise tout projet SvelteKit, le reconnaît donc comme une bibliothèque Svelte et compile ses composants pour le serveur et pour le navigateur. Les composants nécessitent Svelte 5 ; le guide Svelte détaille les composants et leurs props.

Enregistrer les noms d'icônes dans le layout racine

Les composants comme HomeIcon enregistrent leurs propres données. Les noms qui n'apparaissent que dans des chaînes, dans Icon, Fusion, MorphIcon ou morphTo, nécessitent registerIcons, et le meilleur endroit pour cela est un module importé par le layout racine : il s'exécute sur le serveur avant le premier rendu, et dans le navigateur avant l'hydratation.

src/lib/icons.js
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]);
src/routes/+layout.svelte
<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()}

Le registre est un état de module. Sur le serveur, il est partagé par toutes les requêtes, ce qui ne pose aucun problème puisque les données des icônes ne changent jamais, et il est rempli une seule fois, au premier chargement du module.

Ce que rend le serveur

Les composants d'icônes produisent leur svg complet pendant le rendu côté serveur. Les animations des états vivants sont une feuille de style intégrée au svg : un spinner 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 clic et de survol démarrent après l'hydratation, car $effect ne s'exécute pas sur le serveur.

Pour un morph dont l'extrémité à afficher dépend des données, utilisez trigger="state" avec active. Le serveur rend la bonne extrémité du morph, et l'animation ne se joue que lorsque la valeur change dans le navigateur. Avec le déclencheur par défaut (mount), le serveur rend l'icône de départ et le morph se joue à chaque chargement de page.

src/routes/+layout.server.js
export function load({ cookies }) {
  return { theme: cookies.get('theme') ?? 'light' };
}
src/lib/ThemeToggle.svelte
<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>

Lire le thème depuis un cookie dans une fonction load côté serveur donne au serveur et au navigateur la même valeur de départ. Rendez ce composant depuis le layout racine avec <ThemeToggle theme={data.theme} /> ; moon est enregistré dans src/lib/icons.js.

Des fichiers SVG servis par un endpoint

toSvg, issu du point d'entrée principal glyphfox, rend n'importe quelle spécification en chaîne, ce qui permet d'écrire une route +server.js qui sert des icônes sous forme de fichiers : un favicon, une image pour un modèle d'e-mail ou un aperçu de lien. Avec entries, les fichiers sont prérendus au moment du build.

src/routes/icons/[name].svg/+server.js
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' } });
}
src/app.html
<link rel="icon" href="/icons/fox.svg" type="image/svg+xml" />

Un fichier SVG autonome n'a pas de parent dont il pourrait hériter une couleur : passez donc color, dont la valeur est écrite sur le svg et reprise par les traits en currentColor. Renvoyez iconNames.map((name) => ({ name })) depuis entries pour prérendre toutes les icônes.

Le point d'entrée principal contient les données de toutes les icônes. Ce n'est pas un problème dans les fichiers +server.js et +page.server.js, qui n'atteignent jamais le navigateur ; importé dans un composant, toSvg enverrait toute la collection au client.

Balisage réservé au serveur avec {@html}

Une fonction load dans +page.server.js peut renvoyer des chaînes déjà rendues, que la page insère avec {@html}. Le navigateur reçoit le balisage dans le HTML et dans les données sérialisées de la page, mais aucun code d'icône.

src/routes/+page.server.js
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 }) })),
  };
}
src/routes/+page.svelte
<script>
  let { data } = $props();
</script>

<ul class="features">
  {#each data.features as feature (feature.icon)}
    <li>{@html feature.svg} {feature.title}</li>
  {/each}
</ul>

{@html} est sans danger ici, car les chaînes proviennent de votre propre code serveur, jamais d'une saisie utilisateur. Sans title, les icônes sont rendues avec aria-hidden, ce qui convient à côté d'un libellé visible.

adapter-static et prérendu

Avec adapter-static, ou export const prerender = true sur n'importe quelle route, les pages sont rendues au moment du build et les icônes sont écrites dans les fichiers HTML. Les règles du rendu côté serveur s'appliquent aussi au build : un nom que le build ne peut pas résoudre est écrit sous forme de svg vide.

svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
  kit: { adapter: adapter() },
};
src/routes/+layout.js
export const prerender = true;

Pour les pages qui n'ont pas besoin de JavaScript, ajoutez export const csr = false : les icônes restent du SVG statique et aucun code de composant n'est envoyé. Les animations CSS se jouent toujours, tandis que les morphs et les déclencheurs de clic ou de survol, qui nécessitent JavaScript, restent sans effet.

Les noms dans les builds de production

En développement, Vite pré-bundle le paquet, si bien que n'importe quel nom semble fonctionner. Dans un build de production, Vite n'intègre pas au bundle le chargeur à la demande que glyphfox conserve dans node_modules, car il ignore les imports dynamiques contenant des variables dans les dépendances, et les noms non enregistrés restent vides. Enregistrez-les, ou laissez Vite intégrer le chargeur au bundle, ce qui génère alors un petit chunk par icône :

vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [sveltekit()],
  build: {
    dynamicImportVarsOptions: { exclude: [/node_modules\/(?!glyphfox\/)/] },
  },
});

Ces chunks ne servent que dans le navigateur, après l'hydratation. Le rendu côté serveur ne les attend pas : l'enregistrement reste donc le moyen de faire figurer une icône dans le HTML.

Props

SvelteKit utilise les composants de glyphfox/svelte : les props sont donc les props communes, en camelCase.

PropTypePar défautDescription
sizenumber, string24Largeur et hauteur ; les nombres sont en pixels.
colorstringcurrentColorCouleur principale.
secondaryColorstringcolorCouleur du calque duotone et du remplissage plein.
secondaryOpacitynumber0.3 / 1Opacité du calque duotone ou du remplissage plein.
weightnumber2Trait de 1 à 3 sur la grille de 24, avec compensation optique en dessous de 24 px.
absoluteWeightbooleanfalseGarde le trait à weight pixels quelle que soit la taille.
cornersround, sharproundJointures et extrémités arrondies ou vives.
variantoutline, duotone, solidoutlineAxe de style ; solid vient des formes fermées.
rotatenumber0Rotation en degrés.
flipXbooleanfalseMiroir horizontal.
flipYbooleanfalseMiroir vertical.
mirrorRtlbooleanfalseMiroir dans les mises en page de droite à gauche.
animatedraw, pop, wiggle, spin, pulse, bounce, shakeaucunPréréglage d'état vivant.
triggermount, hover, click, statemountMoment où l'animation ou le morph se joue.
activebooleanfalseL'état pour le déclencheur state.
durationnumber (ms)selon le préréglageDurée de l'animation ou du morph.
delaynumber (ms)0Délai de l'animation ou du morph.
loopboolean, numberfalseRépète à l'infini ou un certain nombre de fois (spin et pulse bouclent par défaut).
modifierplus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dotaucunBadge avec espace de découpe automatique.
modifierCornertop-left, top-right, bottom-left, bottom-rightbottom-rightCoin du badge.
morphToname, dataaucunCible du morph ; le déclencheur décide quand elle s'affiche.
titlestringaucunNom accessible : title, role img et aria-label.
decorativebooleantrue sans titleMasque l'icône aux technologies d'assistance.

Pièges courants

Enregistrer dans onMount ou dans du code réservé au navigateur
Le code placé dans onMount, dans $effect ou derrière if (browser) s'exécute après le rendu du serveur. Le HTML contient alors des icônes vides qui apparaissent brusquement après l'hydratation. Enregistrez les noms dans un module importé par le layout racine.
Une icône différente sur le serveur et dans le navigateur
Le contenu du svg est inséré avec {@html}, et Svelte conserve le balisage serveur d'un bloc {@html} lorsque le premier rendu client diffère (il émet l'avertissement hydration_html_changed en développement). Une icône choisie à partir de localStorage ou de window peut donc rester erronée jusqu'au changement suivant. Déduisez la première valeur de données dont le serveur dispose aussi, comme un cookie lu dans une fonction load.
toSvg dans un composant partagé
Importer toSvg depuis glyphfox dans un fichier .svelte fait entrer les données de toutes les icônes dans le bundle client. Réservez-le aux fichiers serveur, ou utilisez les composants.
Un morph prérendu dans le mauvais état
Avec le déclencheur mount, le HTML prérendu affiche l'icône de départ et le morph se joue après l'hydratation. Utilisez trigger="state" pour tout ce qui doit être prérendu dans son état final.

Questions fréquentes

Puis-je prérendre chaque icône sous forme de fichier SVG ?

Oui. Renvoyez iconNames.map((name) => ({ name })) depuis entries dans l'endpoint ci-dessus, et le build écrit un fichier par icône dans /icons/. Chaque fichier pèse quelques centaines d'octets.

Les icônes fonctionnent-elles sur les pages avec csr = false ?

Oui. Le serveur écrit le svg dans le HTML, et les animations des états vivants se jouent toujours, car il s'agit de CSS intégré au svg. Les morphs et les déclencheurs de clic et de survol nécessitent JavaScript : ils restent donc figés sur leur première image.

Icônes de ce guide