Iconos animados
glyphfox anima los iconos de dos maneras. Los estados vivos son presets como draw, pop o shake que se reproducen cuando se activa un disparador. Los morphs dan a un icono la forma de otro. Ambos funcionan en todos los paquetes y ambos se desactivan solos para quienes piden movimiento reducido.
Configuración
No hay nada extra que instalar: la animación forma parte de las props compartidas. Los ejemplos siguientes usan glyphfox/react; las props se llaman igual en Vue, Svelte, Angular y el web component, donde son atributos en kebab-case.
npm install glyphfox
Un estado vivo son unas pocas líneas de CSS que viajan dentro del propio SVG, en un elemento <style> acotado a una clase generada para ese icono. Por eso la misma animación funciona en un componente, en la cadena que devuelve toSvg, en un data URI o en un archivo exportado, sin ninguna hoja de estilos que importar.
Los siete presets
Cada preset está ajustado para iconos, no para elementos genéricos. draw recorre los trazos en el orden en que se dibujaron y, al final, hace aparecer la capa de relleno con un fundido. shake oscila desde arriba, como una campana colgada de un gancho. Los demás mueven el icono entero alrededor de su centro.
| Preset | Movimiento | Duración | En bucle por defecto |
|---|---|---|---|
draw | dibuja cada trazo, uno tras otro | 1100 ms | no |
pop | crece desde el 55 por ciento con un ligero rebote | 520 ms | no |
wiggle | gira a izquierda y derecha hasta asentarse | 720 ms | no |
spin | una vuelta completa a velocidad constante | 1000 ms | sí |
pulse | se encoge y se atenúa un poco, y luego vuelve | 1500 ms | sí |
bounce | salta hacia arriba y se asienta con dos rebotes más pequeños | 900 ms | no |
shake | oscila desde arriba con una amplitud decreciente | 950 ms | no |
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 y delay se expresan en milisegundos. loop acepta true para repetir sin fin o un número de repeticiones; spin y pulse se repiten en bucle salvo que pases loop={false}. Un draw en bucle alterna la dirección, de modo que el icono se dibuja y luego se borra, en lugar de volver de golpe al principio.
Disparadores
La prop trigger decide cuándo se reproduce un preset:
mount, el valor predeterminado, lo reproduce cuando el icono se renderiza por primera vez.hoverlo reproduce mientras el puntero está sobre el icono o sobre su padre directo, así que un icono dentro de un botón se anima al pasar el puntero por el botón.clicklo reproduce de nuevo con cada clic en el icono.statelo reproduce mientrasactivesea verdadero. Es el disparador que debes usar para todo lo que decide tu aplicación: una notificación nueva, un guardado fallido, una subida terminada.
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> ); }
En el marcado estático, los disparadores se corresponden con simples ganchos de CSS. Una animación state se reproduce mientras el elemento svg tiene la clase gf-on, así que el HTML renderizado en el servidor y las cadenas de toSvg se pueden controlar con una sola línea de script: svg.classList.toggle('gf-on', unread > 0). hover no necesita ningún script.
Transforma un icono en otro
El motor de morph aplana ambos iconos en hebras, las empareja mediante una asignación óptima, las remuestrea con el mismo número de puntos e interpola. Las hebras sin pareja se encogen y se desvanecen. Cualquier par de iconos puede transformarse; diez pares están ajustados a mano para que parezcan diseñados y no calculados:
| Origen | Destino |
|---|---|
| menu | x |
| play | pause |
| plus | check |
| sun | moon |
| eye | eye-off |
| bell | bell-ring |
| heart | heart en estilo sólido |
| copy | check |
| lock | unlock |
| wifi | wifi-off |
Hay tres formas de pedir un morph. MorphIcon se transforma cada vez que cambia su name. morphTo, en cualquier icono, lo transforma hacia un destino cuando se activa su disparador y lo devuelve a su forma original cuando este se desactiva. Los hooks y stores (useMorph en React y Vue, morphStore en Svelte) te entregan los fotogramas para que los renderices tú mismo.
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> ); }
Los morphs duran 450 ms salvo que pases duration. Con trigger="state", el icono sigue a active en ambas direcciones, lo que encaja con controles de dos estados como un menú, un campo de contraseña (de eye a eye-off) o un botón de reproducción.
Los mismos morphs en Vue, Svelte y el 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>
El atributo morph hace que <glyph-fox> anime cada cambio de name. La versión de la CDN ya incluye todos los iconos, así que ahí no hace falta registrar nada.
Morphs sin JavaScript
toSvg con morphTo devuelve un SVG cuyos trazados contienen la forma inicial y pasan a la de destino mediante una transición CSS de d. Los navegadores que no admiten d en CSS hacen, en su lugar, un fundido cruzado hacia el destino exacto. El resultado es una cadena normal, lista para plantillas de servidor, sitios estáticos o un 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' });
La CLI genera el mismo archivo: npx glyphfox morph menu x --trigger hover --out menu.svg. Solo toSvg resuelve un destino indicado por su nombre; renderToString de glyphfox/core espera datos de icono en morphTo.
Clases de animación de Tailwind CSS
Los iconos renderizados con el plugin de Tailwind CSS son máscaras, no SVG, así que admiten animaciones del elemento completo: gf-spin, gf-pulse, gf-pop, gf-wiggle, gf-bounce y gf-shake, con los mismos tiempos que los presets. Funcionan con variantes como hover:gf-wiggle. Draw y los morphs necesitan SVG en línea.
Movimiento reducido
Cada preset incluye una regla prefers-reduced-motion: reduce que desactiva su animación, el morph solo con CSS cambia sin transición y los morphs con JavaScript de los componentes saltan directamente a la forma final. No necesitas comprobar el ajuste tú mismo. Quienes lo activan siguen viendo el estado final: el corazón sólido, la x de un menú abierto, la marca de verificación tras copiar.
El movimiento reducido no es la única razón para que las animaciones sean breves. El movimiento atrae la mirada, así que reserva las animaciones en bucle para lo que está en curso, como un loader-circle que gira mientras se ejecuta una solicitud, y detenlas cuando termine el trabajo. La guía de accesibilidad profundiza en el movimiento y el significado.
Exporta archivos animados
El personalizador del explorador de iconos exporta cualquier icono con un preset como SVG animado, GIF o video WebM, con exactamente el mismo movimiento: los fotogramas del GIF y del WebM se muestrean a partir de los tiempos del preset. Usa el SVG animado allí donde se acepte SVG, y GIF o WebM para diapositivas, herramientas de documentación y aplicaciones de chat que eliminan el SVG.
Rendimiento
- Los presets distintos de
drawsolo animantransformyopacity, que los navegadores ejecutan sin recalcular el layout.drawanimastroke-dashoffset, lo que vuelve a pintar el icono en cada fotograma: está bien para unos pocos iconos, pero conviene evitarlo en listas largas. - Los morphs de los componentes se ejecutan con
requestAnimationFramey vuelven a renderizar el icono en cada fotograma mientras dura el morph. Unos pocos a la vez salen baratos; cientos que empiezan juntos, no. - Los nombres de clase generados son hashes de la configuración, así que iconos idénticos producen un marcado idéntico y la salida del servidor coincide con la del cliente.
Props de animación
Estas son las props que controlan el movimiento. Cualquier otra prop compartida, como variant o weight, se puede combinar con ellas.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
animate | draw, pop, wiggle, spin, pulse, bounce, shake | ninguno | Preset de estado vivo. |
trigger | mount, hover, click, state | mount | Cuándo se reproduce la animación o el morph. |
active | boolean | false | El estado para el disparador state. |
duration | number (ms) | según el preset | Duración de la animación o del morph. |
delay | number (ms) | 0 | Retraso de la animación o del morph. |
loop | boolean, number | false | Repite sin fin o un número de veces (spin y pulse se repiten por defecto). |
morphTo | name, data | ninguno | Destino del morph; el disparador decide cuándo se muestra. |
Errores comunes
- Destinos de morph que no renderizan nada
- Los destinos de morph, los nombres de
MorphIcony los demorphStorese buscan por nombre. Un nombre cuyos datos nunca se importaron ni se registraron se carga bajo demanda, y en un build de producción de Vite esa carga no ocurre, porque Vite deja intacta la importación dinámica con variable que hay dentro denode_modules. El destino queda vacío. Registra conregisterIconstodos los nombres que uses como destino de un morph. Importar un componente comoCheckIconsolo por su efecto secundario no sirve, porque el tree shaking elimina un import que nunca se usa. - Hover y clic fuera del icono
- Un preset hover en SVG en línea también responde al padre directo, gracias a CSS. No ocurre lo mismo con todo lo que funciona mediante eventos:
trigger="click"y el disparador hover de un morph escuchan en el propio icono, yglyph-foxsolo reacciona al puntero sobre el propio elemento. Un clic en el padding de un botón no les llega. Guarda el estado en el botón y pásalo al icono contrigger="state"yactive. - Animaciones de montaje que se repiten
- Una animación
mountse reproduce cada vez que el icono se monta: cuando una lista se vuelve a renderizar con keys nuevas, cuando cambia la ruta y, englyph-fox, cada vez que cambia uno de sus atributos. Si un icono debe animarse una sola vez, usatrigger="state"y decide en tu código cuándo se reproduce. - Animaciones de estado que nunca se detienen
- Un preset con
loopsigue reproduciéndose mientrasactivesiga siendo verdadero. Vuelve a poneractiveen falso cuando se haya gestionado el evento, o asigna un número aloop. - Marcado estático y clics
- En el HTML generado con
toSvg, nada escucha los clics. Usatrigger: 'state'y alterna la clasegf-ondesde tu propio manejador.
Preguntas frecuentes
¿Los iconos animados necesitan JavaScript?
Los estados vivos no: el CSS está dentro del SVG, así que un icono estático o renderizado en el servidor se anima al montarse y al pasar el puntero sin ningún script. El morph solo con CSS de toSvg también funciona sin framework. Los morphs de componentes como MorphIcon usan JavaScript para calcular los fotogramas.
¿Un icono puede transformarse y reproducir un preset a la vez?
Sí. animate y morphTo son props independientes, así que un corazón puede reproducir pop con un clic mientras se transforma a su estilo sólido: <HeartIcon animate="pop" morphTo="heart/solid" trigger="click" />.