Í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.
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.
| Preset | Movimento | Duração | Repete por padrão |
|---|---|---|---|
draw | desenha cada traço, um depois do outro | 1100 ms | não |
pop | cresce a partir de 55 por cento, ultrapassando de leve o tamanho final | 520 ms | não |
wiggle | gira para a esquerda e para a direita até se acomodar | 720 ms | não |
spin | uma volta completa em velocidade constante | 1000 ms | sim |
pulse | encolhe e esmaece um pouco, depois volta | 1500 ms | sim |
bounce | pula para cima e depois se acomoda com dois quiques menores | 900 ms | não |
shake | balança a partir do topo com amplitude decrescente | 950 ms | não |
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.hovertoca 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.clicktoca de novo a cada clique no ícone.statetoca enquantoactivefor 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.
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:
| De | Para |
|---|---|
| menu | x |
| play | pause |
| plus | check |
| sun | moon |
| eye | eye-off |
| bell | bell-ring |
| heart | heart no estilo sólido |
| copy | check |
| lock | unlock |
| wifi | wifi-off |
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.
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
<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>
<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>
<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.
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 apenastransformeopacity, que os navegadores executam sem recalcular o layout.drawanimastroke-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
requestAnimationFramee 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.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
animate | draw, pop, wiggle, spin, pulse, bounce, shake | nenhum | Animação de estado vivo. |
trigger | mount, hover, click, state | mount | Quando a animação ou o morph acontece. |
active | boolean | false | O estado para o gatilho state. |
duration | number (ms) | por animação | Duração da animação ou do morph. |
delay | number (ms) | 0 | Atraso da animação ou do morph. |
loop | boolean, number | false | Repete para sempre ou um número de vezes (spin e pulse repetem por padrão). |
morphTo | name, data | nenhum | Destino do morph; o gatilho decide quando aparece. |
Armadilhas comuns
- Destinos de morph que não renderizam nada
- Destinos de morph, nomes em
MorphIcone nomes emmorphStoresã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 denode_modules. O destino fica vazio. Registre comregisterIconstodos os nomes que você usa como destino de morph. Importar um componente comoCheckIconsó 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 oglyph-foxsó 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 comtrigger="state"eactive. - Animações de montagem que tocam de novo
- Uma animação
mounttoca toda vez que o ícone é montado: quando uma lista é renderizada de novo com keys novas, quando a rota muda e, noglyph-fox, sempre que um dos atributos dele muda. Se um ícone deve animar uma única vez, usetrigger="state"e decida no seu código quando a animação toca. - Animações de estado que nunca param
- Um preset com
loopcontinua tocando enquantoactivepermanecer verdadeiro. Volteactivepara falso quando o evento for tratado, ou passe um número paraloop. - Markup estático e cliques
- No HTML gerado por
toSvg, nada escuta cliques. Usetrigger: 'state'e alterne a classegf-onno 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" />.