Thèmes

Icônes animées

glyphfox anime les icônes de deux façons. Les états vivants sont des préréglages, comme draw, pop ou shake, qui se jouent en réponse à un déclencheur. Les morphs transforment une icône en une autre. Les deux fonctionnent dans tous les paquets et se désactivent d'eux-mêmes pour les personnes qui demandent un mouvement réduit.

Mise en place

Rien de plus à installer : l'animation fait partie des props communes. Les exemples ci-dessous utilisent glyphfox/react ; les props portent les mêmes noms dans Vue, Svelte, Angular et le web component, où elles prennent la forme d'attributs en kebab-case.

terminal
npm install glyphfox

Un état vivant, ce sont quelques lignes de CSS embarquées dans le SVG lui-même, dans un élément <style> dont la portée est limitée à une classe générée pour cette icône. C'est pourquoi la même animation fonctionne dans un composant, dans la chaîne renvoyée par toSvg, dans une data URI ou dans un fichier exporté, sans aucune feuille de style à importer.

Les sept préréglages

Chaque préréglage est pensé pour des icônes plutôt que pour des éléments quelconques. draw trace les traits dans l'ordre où ils ont été dessinés et fait apparaître le calque de remplissage en fondu à la fin. shake oscille depuis le haut, comme une cloche suspendue à son crochet. Les autres animent l'icône entière autour de son centre.

PréréglageMouvementDuréeEn boucle par défaut
drawtrace chaque trait, l'un après l'autre1100 msnon
popgrandit à partir de 55 % avec un léger dépassement520 msnon
wigglepivote à gauche et à droite en s'amortissant720 msnon
spintour complet à vitesse constante1000 msoui
pulserétrécit et s'estompe légèrement, puis revient1500 msoui
bouncesaute vers le haut, puis se pose avec deux rebonds plus petits900 msnon
shakeoscille depuis le haut avec une amplitude décroissante950 msnon
Presets.jsx
import { LoaderCircleIcon, SparklesIcon, BellIcon, HeartIcon } from 'glyphfox/react';

export function Presets({ hasNews, liked }) {
  return (
    <>
      <LoaderCircleIcon animate="spin" />
      <SparklesIcon animate="draw" />
      <BellIcon animate="shake" trigger="state" active={hasNews} />
      <HeartIcon animate="pop" trigger="click" variant={liked ? 'solid' : 'outline'} />
    </>
  );
}

duration et delay s'expriment en millisecondes. loop accepte true pour une boucle infinie ou un nombre d'exécutions ; spin et pulse s'exécutent en boucle, sauf si vous passez loop={false}. Un draw en boucle alterne le sens : l'icône se dessine puis s'efface, au lieu de revenir brusquement au début.

Déclencheurs

La prop trigger détermine quand un préréglage se joue :

  • mount, la valeur par défaut, se joue au premier rendu de l'icône.
  • hover se joue tant que le pointeur survole l'icône ou son parent direct : une icône placée dans un bouton s'anime donc au survol du bouton.
  • click se rejoue à chaque clic sur l'icône.
  • state se joue tant que active vaut true. C'est le déclencheur à utiliser pour tout ce que décide votre application : une nouvelle notification, un échec d'enregistrement, un envoi de fichier terminé.
Notifications.jsx
import { BellIcon } from 'glyphfox/react';

export function NotificationsButton({ unread }) {
  return (
    <button type="button" aria-label={`Notifications, ${unread} unread`}>
      <BellIcon animate="shake" trigger="state" active={unread > 0} loop={2} />
    </button>
  );
}

Dans un balisage statique, les déclencheurs correspondent à de simples points d'accroche CSS. Une animation state se joue tant que l'élément svg porte la classe gf-on. Le HTML rendu côté serveur et les chaînes issues de toSvg se pilotent donc avec une seule ligne de script : svg.classList.toggle('gf-on', unread > 0). hover ne demande aucun script.

Transformer une icône en une autre

Le moteur de morph aplatit les deux icônes en brins, les apparie par une affectation optimale, les rééchantillonne au même nombre de points, puis interpole. Les brins sans partenaire rétrécissent et s'estompent. Deux icônes quelconques peuvent se transformer l'une en l'autre ; dix paires sont réglées à la main pour sembler conçues plutôt que calculées :

Il existe trois façons de demander un morph. MorphIcon se transforme chaque fois que sa prop name change. Placée sur n'importe quelle icône, la prop morphTo transforme l'icône en une cible quand son déclencheur s'active, puis lui rend sa forme d'origine quand il s'arrête. Les hooks et les stores (useMorph dans React et Vue, morphStore dans Svelte) vous fournissent les images de l'animation pour votre propre rendu.

CopyButton.jsx
import { useState } from 'react';
import { MorphIcon, MenuIcon, registerIcons } from 'glyphfox/react';
import { iconCopy, iconCheck, iconX } from 'glyphfox';

// Morph targets are looked up by name: register their data once.
registerIcons([iconCopy, iconCheck, iconX]);

export function CopyButton({ text }) {
  const [copied, setCopied] = useState(false);
  const copy = async () => {
    await navigator.clipboard.writeText(text);
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  };
  return (
    <button type="button" onClick={copy}>
      <MorphIcon name={copied ? 'check' : 'copy'} duration={300} />
      {copied ? 'Copied' : 'Copy'}
    </button>
  );
}

export function MenuButton({ open, onToggle }) {
  return (
    <button type="button" aria-expanded={open} aria-label="Menu" onClick={onToggle}>
      <MenuIcon morphTo="x" trigger="state" active={open} />
    </button>
  );
}

Un morph dure 450 ms, sauf si vous passez duration. Avec trigger="state", l'icône suit active dans les deux sens, ce qui convient aux éléments à bascule comme un menu, un champ de mot de passe (eye vers eye-off) ou un bouton de lecture.

Les mêmes morphs dans Vue, Svelte et le web component

PlayButton.vue
<script setup>
import { ref } from 'vue';
import { MorphIcon, registerIcons } from 'glyphfox/vue';
import { iconPlay, iconPause } from 'glyphfox';

registerIcons([iconPlay, iconPause]);
const playing = ref(false);
</script>

<template>
  <button type="button" :aria-label="playing ? 'Pause' : 'Play'" @click="playing = !playing">
    <MorphIcon :name="playing ? 'pause' : 'play'" />
  </button>
</template>
ThemeToggle.svelte
<script>
  import { Icon, morphStore, registerIcons } from 'glyphfox/svelte';
  import { iconSun, iconMoon } from 'glyphfox';

  registerIcons([iconSun, iconMoon]);
  const icon = morphStore('sun', { duration: 400 });
  let dark = $state(false);
</script>

<button type="button" aria-label="Dark theme" aria-pressed={dark}
  onclick={() => { dark = !dark; icon.set(dark ? 'moon' : 'sun'); }}>
  {#if $icon}<Icon icon={$icon.icon} variant={$icon.variant} />{/if}
</button>
index.html
<script src="https://cdn.jsdelivr.net/npm/glyphfox@1"></script>

<button type="button" id="menu" aria-expanded="false" aria-label="Menu">
  <glyph-fox name="menu" morph duration="300"></glyph-fox>
</button>

<script>
  const button = document.getElementById('menu');
  const icon = button.querySelector('glyph-fox');
  button.addEventListener('click', () => {
    const open = button.getAttribute('aria-expanded') !== 'true';
    button.setAttribute('aria-expanded', String(open));
    icon.setAttribute('name', open ? 'x' : 'menu');
  });
</script>

Avec l'attribut morph, <glyph-fox> anime chaque changement de name. Le build CDN contient déjà toutes les icônes : aucun enregistrement n'y est nécessaire.

Morphs sans JavaScript

toSvg avec morphTo renvoie un SVG dont les tracés portent la forme de départ et passent à la cible grâce à une transition CSS sur d. Les navigateurs qui ne prennent pas en charge d en CSS effectuent à la place un fondu enchaîné vers la cible exacte. Le résultat est une simple chaîne, prête pour les templates côté serveur, les sites statiques ou un CMS.

build-icons.js
import { toSvg } from 'glyphfox';

// Morphs while the pointer is over the icon or its direct parent.
const menu = toSvg('menu', { morphTo: 'x', trigger: 'hover', duration: 300 });

// Morphs while the svg has the class gf-on: toggle it from any script.
const play = toSvg('play', { morphTo: 'pause', trigger: 'state' });

La CLI génère le même fichier : npx glyphfox morph menu x --trigger hover --out menu.svg. Seul toSvg résout une cible donnée par son nom ; renderToString de glyphfox/core attend des données d'icône pour morphTo.

Classes d'animation Tailwind CSS

Les icônes rendues avec le plugin Tailwind CSS sont des masques et non du SVG : elles acceptent donc des animations de l'élément entier (gf-spin, gf-pulse, gf-pop, gf-wiggle, gf-bounce et gf-shake), avec le même minutage que les préréglages. Elles fonctionnent avec des variantes comme hover:gf-wiggle. L'animation draw et les morphs nécessitent du SVG inline.

Mouvement réduit

Chaque préréglage embarque une règle prefers-reduced-motion: reduce qui désactive son animation, le morph en CSS pur bascule sans transition, et les morphs JavaScript des composants passent directement à la forme finale. Vous n'avez pas besoin de vérifier ce réglage vous-même. Les personnes qui l'ont activé voient tout de même l'état final : le cœur plein, le x d'un menu ouvert, la coche après une copie.

Le mouvement réduit n'est pas la seule raison de garder les animations courtes. Le mouvement attire l'œil : réservez donc les animations en boucle aux opérations en cours, comme un loader-circle qui tourne pendant l'exécution d'une requête, et arrêtez-les une fois le travail terminé. Le guide d'accessibilité approfondit la question du mouvement et du sens.

Exporter des fichiers animés

L'outil de personnalisation de l'explorateur d'icônes exporte n'importe quelle icône dotée d'un préréglage en SVG animé, en GIF ou en vidéo WebM, avec exactement le même mouvement : les images du GIF et du WebM sont échantillonnées à partir du minutage du préréglage. Utilisez le SVG animé partout où le SVG est accepté, et le GIF ou le WebM pour les présentations, les outils de documentation et les messageries qui suppriment le SVG.

Performances

  • Les préréglages autres que draw n'animent que transform et opacity, que les navigateurs traitent sans recalcul de mise en page. draw anime stroke-dashoffset, ce qui redessine l'icône à chaque image : sans problème pour quelques icônes, mais à éviter dans les longues listes.
  • Les morphs des composants reposent sur requestAnimationFrame et refont le rendu de l'icône à chaque image pendant toute la durée du morph. Quelques-uns à la fois ne coûtent presque rien ; des centaines lancés en même temps, si.
  • Les noms de classes générés sont des hachages des réglages : des icônes identiques produisent donc un balisage identique, et les rendus serveur et client concordent.

Props d'animation

Voici les props qui contrôlent le mouvement. Toutes les autres props communes, comme variant ou weight, peuvent s'y combiner.

PropTypePar défautDescription
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).
morphToname, dataaucunCible du morph ; le déclencheur décide quand elle s'affiche.

Pièges courants

Cibles de morph qui n'affichent rien
Les cibles de morph, comme les noms passés à MorphIcon et à morphStore, sont recherchées par nom. Un nom dont les données n'ont jamais été importées ni enregistrées est chargé à la demande, et dans un build de production Vite ce chargement n'a pas lieu, car Vite laisse intact l'import dynamique à chemin variable situé dans node_modules. La cible reste vide. Enregistrez avec registerIcons chaque nom vers lequel une icône se transforme. Importer un composant comme CheckIcon uniquement pour son effet de bord ne sert à rien, car le tree shaking supprime tout import jamais utilisé.
Survol et clic en dehors de l'icône
Sur du SVG inline, un préréglage au survol réagit aussi au parent direct, grâce au CSS. Ce n'est pas le cas de tout ce qui repose sur des événements : trigger="click" et le déclencheur hover d'un morph écoutent les événements de l'icône elle-même, et glyph-fox ne réagit qu'au pointeur sur l'élément lui-même. Un clic dans le padding d'un bouton ne les atteint pas. Gardez l'état dans le bouton et transmettez-le à l'icône avec trigger="state" et active.
Animations au montage qui se rejouent
Une animation mount se joue à chaque montage de l'icône : quand une liste est rendue à nouveau avec de nouvelles clés, quand la route change, et sur glyph-fox chaque fois que l'un de ses attributs change. Si une icône ne doit s'animer qu'une fois, utilisez trigger="state" et décidez dans votre code du moment où elle se joue.
Animations d'état qui ne s'arrêtent jamais
Un préréglage avec loop continue de se jouer tant que active reste à true. Remettez active à false une fois l'événement traité, ou donnez un nombre à loop.
Balisage statique et clics
Dans le HTML produit par toSvg, rien n'écoute les clics. Utilisez trigger: 'state' et basculez la classe gf-on depuis votre propre gestionnaire d'événements.

Questions fréquentes

Les icônes animées nécessitent-elles JavaScript ?

Pas les états vivants : le CSS se trouve dans le SVG, si bien qu'une icône rendue côté serveur ou statique s'anime au montage et au survol sans aucun script. Le morph en CSS pur de toSvg fonctionne lui aussi sans framework. Les morphs des composants, comme MorphIcon, utilisent JavaScript pour calculer les images.

Une icône peut-elle se transformer et jouer un préréglage en même temps ?

Oui. animate et morphTo sont des props indépendantes, si bien qu'un cœur peut jouer l'animation pop au clic tout en se transformant en cœur plein : <HeartIcon animate="pop" morphTo="heart/solid" trigger="click" />.

Icônes de ce guide