Tópicos

Ícones animados

O glyphfox anima ícones de duas formas. Os estados vivos são presets como draw, pop ou shake, que tocam quando um gatilho dispara. Os morphs transformam um ícone em outro. Os dois funcionam em todos os pacotes e se desligam sozinhos para quem pede movimento reduzido.

Configuração

Não há nada extra para instalar: a animação faz parte das props compartilhadas. Os exemplos abaixo usam glyphfox/react; as props têm os mesmos nomes no Vue, no Svelte, no Angular e no web component, onde viram atributos em kebab-case.

terminal
npm install glyphfox

Um estado vivo é formado por algumas linhas de CSS que viajam dentro do próprio SVG, em um elemento <style> com escopo restrito a uma classe gerada para aquele ícone. Por isso a mesma animação funciona em um componente, na string retornada por toSvg, em um data URI ou em um arquivo exportado, sem nenhuma folha de estilo para importar.

Os sete presets

Cada preset foi ajustado para ícones, e não para elementos genéricos. draw desenha os traços na ordem do desenho e faz a camada de preenchimento surgir aos poucos no final. shake balança a partir do topo, como um sino pendurado num gancho. Os outros movem o ícone inteiro em torno do próprio centro.

PresetMovimentoDuraçãoRepete por padrão
drawdesenha cada traço, um depois do outro1100 msnão
popcresce a partir de 55 por cento, ultrapassando de leve o tamanho final520 msnão
wigglegira para a esquerda e para a direita até se acomodar720 msnão
spinuma volta completa em velocidade constante1000 mssim
pulseencolhe e esmaece um pouco, depois volta1500 mssim
bouncepula para cima e depois se acomoda com dois quiques menores900 msnão
shakebalança a partir do topo com amplitude decrescente950 msnão
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 e delay recebem milissegundos. loop recebe true para repetir para sempre ou um número de execuções; spin e pulse repetem, a menos que você passe loop={false}. Um draw em loop alterna a direção, então o ícone se desenha e depois se apaga, em vez de voltar de uma vez ao início.

Gatilhos

A prop trigger decide quando um preset toca:

  • mount, o padrão, toca quando o ícone é renderizado pela primeira vez.
  • hover toca enquanto o ponteiro está sobre o ícone ou sobre o pai direto dele, então um ícone dentro de um botão anima quando o ponteiro passa sobre o botão.
  • click toca de novo a cada clique no ícone.
  • state toca enquanto active for verdadeiro. É o gatilho certo para tudo o que a sua aplicação decide: uma notificação nova, um salvamento que falhou, um upload concluído.
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>
  );
}

Em markup estático, os gatilhos viram ganchos simples de CSS. Uma animação state toca enquanto o elemento svg tiver a classe gf-on, então HTML renderizado no servidor e strings de toSvg podem ser controlados com uma linha de script: svg.classList.toggle('gf-on', unread > 0). hover não precisa de script nenhum.

Transforme um ícone em outro

O motor de morph achata os dois ícones em fios, pareia os fios com uma atribuição ótima, reamostra todos com o mesmo número de pontos e interpola. Fios sem par encolhem e somem. Quaisquer dois ícones podem se transformar um no outro; dez pares foram ajustados à mão para parecerem desenhados, e não calculados:

Há três formas de pedir um morph. MorphIcon se transforma sempre que o name dele muda. morphTo, em qualquer ícone, faz o morph até um destino quando o gatilho dispara e volta quando o gatilho termina. Hooks e stores (useMorph no React e no Vue, morphStore no Svelte) entregam os quadros para renderizadores próprios.

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>
  );
}

Os morphs duram 450 ms, a menos que você passe duration. Com trigger="state", o ícone acompanha active nos dois sentidos, o que combina com toggles como um menu, um campo de senha (eye para eye-off) ou um botão de play.

Os mesmos morphs no Vue, no Svelte e no 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>

O atributo morph faz o <glyph-fox> animar toda troca de name. O build da CDN já contém todos os ícones, então nele não é preciso registrar nada.

Morphs sem JavaScript

toSvg com morphTo retorna um SVG cujos paths carregam a forma inicial e mudam para o destino com uma transição CSS de d. Navegadores sem suporte a d no CSS fazem, em vez disso, um cross fade para o destino exato. O resultado é uma string simples, pronta para templates no servidor, sites estáticos ou um 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' });

A CLI gera o mesmo arquivo: npx glyphfox morph menu x --trigger hover --out menu.svg. Só o toSvg resolve um destino passado como nome; o renderToString de glyphfox/core espera dados de ícone em morphTo.

Classes de animação do Tailwind CSS

Os ícones renderizados com o plugin para Tailwind CSS são máscaras, não SVG, então recebem animações aplicadas ao elemento inteiro: gf-spin, gf-pulse, gf-pop, gf-wiggle, gf-bounce e gf-shake, com a mesma temporização dos presets. Elas funcionam com variantes como hover:gf-wiggle. Draw e morphs precisam de SVG inline.

Movimento reduzido

Todo preset traz uma regra prefers-reduced-motion: reduce que desliga a animação, o morph só com CSS troca de forma sem transição e os morphs em JavaScript dos componentes pulam direto para a forma final. Você não precisa verificar essa preferência por conta própria. Quem ativa essa opção continua vendo o estado final: o coração sólido, o x de um menu aberto, o check depois de copiar.

Movimento reduzido não é o único motivo para manter as animações curtas. Movimento chama a atenção, então reserve as animações em loop para coisas em andamento, como um loader-circle que gira enquanto uma requisição está em execução, e pare as animações quando o trabalho terminar. O guia de acessibilidade fala mais sobre movimento e significado.

Exporte arquivos animados

O personalizador do explorador de ícones exporta qualquer ícone com um preset como SVG animado, GIF ou vídeo WebM, com exatamente o mesmo movimento: os quadros do GIF e do WebM são amostrados a partir da temporização do preset. Use o SVG animado onde SVG for aceito, e GIF ou WebM em slides, ferramentas de documentação e apps de chat que removem SVG.

Desempenho

  • Todos os presets, exceto draw, animam apenas transform e opacity, que os navegadores executam sem recalcular o layout. draw anima stroke-dashoffset, o que repinta o ícone a cada quadro: tudo bem para alguns ícones, mas vale evitar em listas longas.
  • Os morphs dos componentes rodam em requestAnimationFrame e renderizam o ícone de novo a cada quadro durante todo o morph. Alguns ao mesmo tempo saem barato; centenas começando juntos, não.
  • Os nomes de classe gerados são hashes das configurações, então ícones idênticos produzem markup idêntico e a saída do servidor bate com a do cliente.

Props de animação

Estas são as props que controlam o movimento. Qualquer outra prop compartilhada, como variant ou weight, pode ser combinada com elas.

PropTipoPadrãoDescrição
animatedraw, pop, wiggle, spin, pulse, bounce, shakenenhumAnimação de estado vivo.
triggermount, hover, click, statemountQuando a animação ou o morph acontece.
activebooleanfalseO estado para o gatilho state.
durationnumber (ms)por animaçãoDuração da animação ou do morph.
delaynumber (ms)0Atraso da animação ou do morph.
loopboolean, numberfalseRepete para sempre ou um número de vezes (spin e pulse repetem por padrão).
morphToname, datanenhumDestino do morph; o gatilho decide quando aparece.

Armadilhas comuns

Destinos de morph que não renderizam nada
Destinos de morph, nomes em MorphIcon e nomes em morphStore são buscados pelo nome. Um nome cujos dados nunca foram importados nem registrados é carregado sob demanda, e no build de produção do Vite esse carregamento não acontece, porque o Vite não mexe no import dinâmico com variável dentro de node_modules. O destino fica vazio. Registre com registerIcons todos os nomes que você usa como destino de morph. Importar um componente como CheckIcon só pelo efeito colateral não adianta, porque o tree shaking remove um import que nunca é usado.
Hover e clique fora do ícone
Um preset de hover em SVG inline também responde ao pai direto, via CSS. Já tudo o que é acionado por eventos, não: trigger="click" e o gatilho de hover de um morph escutam eventos no próprio ícone, e o glyph-fox só reage ao ponteiro sobre o próprio elemento. Um clique no padding de um botão não chega até eles. Mantenha o estado no botão e repasse para o ícone com trigger="state" e active.
Animações de montagem que tocam de novo
Uma animação mount toca toda vez que o ícone é montado: quando uma lista é renderizada de novo com keys novas, quando a rota muda e, no glyph-fox, sempre que um dos atributos dele muda. Se um ícone deve animar uma única vez, use trigger="state" e decida no seu código quando a animação toca.
Animações de estado que nunca param
Um preset com loop continua tocando enquanto active permanecer verdadeiro. Volte active para falso quando o evento for tratado, ou passe um número para loop.
Markup estático e cliques
No HTML gerado por toSvg, nada escuta cliques. Use trigger: 'state' e alterne a classe gf-on no seu próprio handler.

Perguntas frequentes

Ícones animados precisam de JavaScript?

Os estados vivos, não: o CSS fica dentro do SVG, então um ícone estático ou renderizado no servidor anima na montagem e no hover sem nenhum script. O morph só com CSS do toSvg também roda sem framework. Já os morphs de componentes, como o MorphIcon, usam JavaScript para calcular os quadros.

Um ícone pode se transformar e tocar um preset ao mesmo tempo?

Sim. animate e morphTo são props independentes, então um coração pode fazer pop no clique enquanto se transforma no estilo sólido: <HeartIcon animate="pop" morphTo="heart/solid" trigger="click" />.

Ícones deste guia