Icone Lucide animate: quanto costa Animate UI
Un hover che risponde: la differenza fra un sito corretto e un sito che trattiene
6 min di lettura
La pagina è finita e sembra spenta
Il momento è sempre lo stesso. La landing è pronta, il Lighthouse è verde, il contenuto dice quello che deve dire. La apri, la guardi, e non succede niente.
Le icone ci sono già. In questo sito lucide-react c'è dalla prima riga di
codice, e fa esattamente il suo lavoro: disegna una freccia accanto a una CTA,
una busta accanto al form, un chevron accanto a un accordion. Sono ferme. Vanno
benissimo, e non trattengono nessuno.
Quando qualcuno arriva su una pagina di preventivo e ha quindici secondi di attenzione, la differenza fra restare e chiudere la scheda non la fa il contenuto: quello lo legge dopo, se resta. La fa il fatto che la pagina risponda. Una freccia che si sposta di un quarto della sua larghezza quando ci passi sopra costa nulla e comunica una cosa sola, che dall'altra parte c'è qualcuno che ci ha messo attenzione.
Animate UI parte esattamente da lì: ha
preso le icone di Lucide e le ha rifatte con
Motion. Stesso tracciato, stessa griglia da 24, stesso
stroke="currentColor". In più, un'animazione.
Cosa ho digitato e cosa è arrivato
L'ho installata in questo repository, per vedere il costo reale invece di leggerlo nella documentazione.
npx shadcn@latest add @animate-ui/icons-arrow-right✔ Created 4 files:
- src/hooks/use-is-in-view.tsx
- src/components/animate-ui/primitives/animate/slot.tsx
- src/components/animate-ui/icons/icon.tsx
- src/components/animate-ui/icons/arrow-right.tsxQuattro file, 930 righe, e zero righe di differenza nel package.json.
L'unica dipendenza dichiarata è motion, che qui c'era già alla versione
12.26.1 perché il sito anima altre cose. Se stai partendo da zero è un pacchetto
in più, ma è quello, non una libreria di icone.
Delle 930 righe, 655 sono il wrapper condiviso icon.tsx: il contesto, i
trigger, la logica di loop. Si paga una volta. La seconda icona è questa:
npx shadcn@latest add @animate-ui/icons-bell✔ Created 1 file:
ℹ Skipped 3 files: (files might be identical, use --overwrite to overwrite)Ottantasei righe. Prima di arrivarci la CLI mi ha chiesto tre volte se volevo
sovrascrivere index.tsx, che sono i tre file condivisi già installati: la
risposta giusta è no, tre volte, ed è l'unico attrito di tutta l'operazione.
Due numeri da tenere a mente prima di progettarci sopra. Le icone animate sono
260, mentre lucide-react 0.562 esporta più di 1900 moduli: il catalogo
animato è una selezione, non la copia. E la size di default è 28, non 24
come in Lucide, quindi la sostituzione dentro un layout esistente non è
neutrale.
Le tre leve che uso davvero
I props comuni sono tredici, fra trigger, timing e persistenza. In pratica ne tocco tre.
Il primo è animateOnHover, e il modo ovvio di usarlo è anche quello sbagliato:
messo sull'icona, l'animazione parte quando il puntatore entra nell'icona, che è
un bersaglio da venti pixel dentro un bottone da duecento. Quello che vuoi è che
parta sul bottone intero. Per questo esiste il wrapper AnimateIcon con
asChild, che non aggiunge un elemento ma passa i gestori al figlio.
import { AnimateIcon } from "@/components/animate-ui/icons/icon";
import { ArrowRight } from "@/components/animate-ui/icons/arrow-right";
export function Cta() {
return (
<AnimateIcon animateOnHover asChild>
<a href="#contact" className="inline-flex items-center gap-2">
Parliamone
<ArrowRight animation="pointing" size={20} />
</a>
</AnimateIcon>
);
}Il secondo è animation. Ogni icona ne dichiara almeno tre, e arrow-right ne
ha cinque: la default sposta il gruppo del 25% sull'asse x, pointing allunga
la punta accorciando l'asta, out la fa uscire da un lato e rientrare
dall'altro. Su una CTA pointing è più educata, perché il baricentro non si
sposta e il testo accanto non sembra scappare.
Il terzo è completeOnStop, e serve solo con animateOnTap. Senza, al rilascio
del dito l'animazione torna indietro a metà strada e sembra un glitch.
Gli altri dieci props esistono e sono documentati bene. Non li ho usati perché un'icona che si anima al primo scroll in viewport, con un delay e un loop temporizzato, non è più un dettaglio: è una cosa che chiede attenzione, e in una pagina che deve far leggere del testo l'attenzione la vuoi altrove.
Il punto in cui reducedMotion non ti copre
La pagina Accessibility di Animate UI dice di avvolgere l'app in
<MotionConfig reducedMotion="user"> e che così i componenti rispetteranno la
preferenza dell'utente. È il consiglio giusto, ma per le icone è vero solo a
metà, e la ragione sta nel sorgente di Motion.
const positionalKeys = new Set([
"width",
"height",
"top",
"left",
"right",
"bottom",
...transformPropOrder,
]);Quando shouldReduceMotion è attivo, Motion sostituisce la transizione con
{ type: false } solo per le chiavi che stanno in questo Set. Tutto il
resto anima come prima.
Le conseguenze si vedono icona per icona. La bell di default anima rotate e
x, che sono transform: con la preferenza attiva si ferma, correttamente. Ma
arrow-right con animation="pointing" non anima una transform, morfa
l'attributo d da M5 12h14 a M5 12h10. E le due animazioni generiche che
ogni icona eredita, path e path-loop, animano pathLength. Nessuna delle
due è nel Set, quindi continuano a muoversi anche a chi ha chiesto di non farle
muovere.
La rete è una riga.
"use client";
import { useReducedMotion } from "motion/react";
import { Bell } from "@/components/animate-ui/icons/bell";
export function Notifications() {
const reduceMotion = useReducedMotion();
return <Bell animateOnHover loop={!reduceMotion} />;
}Componenti e primitivi, e perché mi sono fermato alle icone
Animate UI non è solo icone. La distribuzione ha anche dei primitivi animati, fra cui port di Radix, Base UI e Headless UI, e dei componenti con uno stile di base ispirato a shadcn/ui costruiti sopra quei primitivi. Sono fatti bene e vale la pena guardarli.
Non li ho provati qui, per un motivo che non ha a che fare con la qualità. Adottare un accordion o un dialog di un'altra distribuzione significa avere due strati di componenti che risolvono lo stesso problema, con due convenzioni di varianti e due modi di gestire il focus, e prima o poi qualcuno importa quello sbagliato. È una decisione di architettura, e va presa in quel modo.
Un'icona no. Un'icona è una foglia: non ha stato, non ha figli, non decide niente per nessun altro. La provi su una CTA, se non ti piace cancelli un file e il progetto non se ne accorge. È la ragione per cui, di tutta la libreria, le icone sono la parte che si prova davvero. Quando invece l'animazione è il prodotto e non il dettaglio, il problema è un altro, e si risolve altrove.
Una cosa da sapere prima di partire: le icone sono dichiarate in beta nella
documentazione, e i file sono copiati dentro il tuo repository. Vuol dire che un
fix a monte non ti arriva da solo, e che l'aggiornamento è un add di nuovo,
file per file. Su tre icone è niente. Su trenta è un lavoro.
La prossima volta che apri una nav, un bottone di CTA o una card con la freccia
in fondo, quella è l'icona da cui partire. Una sola, con animateOnHover sul
bottone intero. Se dopo un giorno che la guardi non ti dà fastidio, ne aggiungi
una seconda.
Hai un problema simile?
Se stai affrontando qualcosa di analogo sul tuo prodotto, scrivimi: nel peggiore dei casi ti do un'opinione gratis.
ParliamoneDa leggere dopo
Il companion mobile che mancava a Coolify
Applicazioni, deploy e log in una app sola: il server self-hosted smette di richiedere una scrivania
5 min di lettura