Real shipped codeTalent platform · SaaSUX & motion engineering

UX e animazioni per creare un prodotto che gli utenti vogliono usare.

Rifinire l'esperienza non è quasi mai la priorità: le animazioni sembrano un lusso, il feedback visivo qualcosa che si fa «dopo». Eppure è proprio lì che si gioca la differenza tra un prodotto che si deve usare e uno che si vuole usare. Questa è la storia di una rifinitura sistematica del movimento — e non te la racconto soltanto: le demo sono vive, si cliccano.

ClienteSeVedemo · Talent platform
RuoloProduct Engineer
Durata1 mese
StackSolid.js · Motion · Kobalte · Tailwind
01 · Contesto

Spostare il focus sull'utente.

Dopo mesi passati a costruire feature, la piattaforma funzionava — ma rifinire l'esperienza non era mai stata la priorità. Eppure anche un prodotto digitale è un oggetto fisico: pop-up, bottoni e modali nascono da qualche parte, reagiscono quando li tocchi, si spostano, escono di scena. Il movimento è l'informazione che li tiene insieme — dice da dove nasce una cosa, dove va, cosa è cambiato — e quando manca l'utente fa più fatica, anche se non sa dirlo.

02 · Problema

L'utente non riceveva le giuste risposte.

Prima di scrivere una riga di CSS ho fatto un censimento del movimento esistente: cosa si muoveva, con quale curva, e cosa spariva e basta. Il quadro era coerente — nel senso peggiore: quasi tutto usava i default del browser, o niente.

1

Nessuna risposta visiva. In tutta l'app non esisteva un solo stato attivo sui bottoni: premere non produceva alcuna reazione. Su mobile, o con la rete lenta, l'utente non aveva modo di sapere se il click fosse arrivato — e premeva di nuovo.

2

Elementi che si muovevano in modo innaturale. Menu, tooltip e popover si espandevano dal proprio centro, materializzati a mezz'aria, staccati dal pulsante premuto. Le tendine entravano con l'easing di default e in chiusura venivano distrutte all'istante — nell'ATS il pannello si svuotava prima ancora di sparire. Niente si muoveva come farebbe un oggetto reale.

3

Accessibilità e performance. La preferenza «riduci movimento» era ignorata o applicata a caso, gli item della sidebar compressa restavano senza tooltip, i menu lunghi sforavano il bordo dello schermo. E transition-all animava qualunque proprietà, comprese quelle che costringono il browser a ricalcolare il layout a ogni frame.

03 · Risultati

Pochi numeri, tanta differenza sotto il dito.

0 file hanno guadagnato il feedback al tocco grazie a un solo componente condiviso
0ms di uscita di scena per le tendine, sulla stessa curva con cui entrano
0 transition-all rimasti nel codice: liste esplicite, solo proprietà che la GPU anima senza ricalcolare il layout

Il risultato vero di una rifinitura non sta in un funnel: sta nel fatto che l'interfaccia adesso risponde. Per questo, poco più sotto, il caso studio non si legge soltanto: le demo sono vive, con le stesse curve e le stesse durate andate in produzione. Si cliccano.

04 · Soluzione

Rendere il movimento portatore di informazioni.

Il principio alla base di ogni intervento: il movimento deve dare un'informazione — da dove nasce una cosa, dove va, cosa è cambiato — mai attirare l'attenzione su di sé. Il filo conduttore è una curva sola, quella delle tendine di iOS: parte decisa, si posa morbida. Definita una volta nei token, usata ovunque.

Un vocabolario di movimento condiviso. Due curve con un nome — ease-drawer per pannelli e tendine, un ease-out deciso per i micro-feedback — e durate standard: 160ms per il tocco, 200ms per le uscite, 300ms per i cambi di layout. Le decisioni si prendono una volta sola, non componente per componente.

Componenti condivisi, non pagine. Il feedback al tocco è entrato nel bottone base usato da 162 file: un intervento in un punto solo ha rifinito tutta l'app. Stessa logica per menu, popover, select e tendine: si sistema l'origine, non le mille istanze.

Le uscite esistono. Un pannello che si chiude deve uscire di scena, non essere distrutto: il contenuto sopravvive alla chiusura quel tanto che basta — 200ms — per scivolare via sulla stessa curva con cui è entrato. Chiudere ha un verso, non è più un taglio in montaggio.

05 · Prova tu stesso

Perché parlare quando puoi provare?

Il movimento non si può valutare da uno screenshot. Queste cinque demo ricreano gli interventi principali con i valori reali andati in produzione — stesse curve, stesse durate, stesse origini. Gli scenari sono separati: prova il prima, poi il dopo, e senti la differenza.

cubic-bezier(0.32, 0.72, 0, 1) La curva delle tendine di iOS: parte decisa, si posa morbida. È il filo conduttore di tutti gli interventi.
Bottoni · feedback al tocco

Adesso rispondono quando li premi.

Prima

Nessun feedback al tocco: in tutta l'app non esisteva un solo stato :active. Su mobile, o con la rete lenta, premere sembrava non fare nulla finché non cambiava schermata.

Dopo

Il bottone condiviso — quello usato da 162 file — si comprime a scale 0.97 mentre lo tieni premuto, in 160ms. Un cenno, non un salto: l'interfaccia ti ha sentito.

Demo · tieni premuto ciascun bottone valori reali: 160ms · cubic-bezier(0.23, 1, 0.32, 1)
Prima · nessuna reazione tieni premuto
Dopo · scale 0.97 tieni premuto
Menu · tooltip · popover · select

Crescono dal punto in cui li apri.

Prima

Comparivano espandendosi dal proprio centro — come materializzati a mezz'aria, staccati dal pulsante premuto.

Dopo

Si espandono a partire dal loro transform-origin reale: sembrano nascere dal bottone che hai toccato.

Demo · clicca uno scenario per volta rallentata ed esagerata per rendere visibile il punto di partenza
Tendine · drawer · sheet

La curva di iOS, non l'easing di default.

Prima

Easing generico del browser: un movimento un po' meccanico, che parte e finisce piatto.

Dopo

La curva ease-drawer: scatto iniziale deciso e atterraggio morbido. La sensazione fisica delle tendine di iOS.

Demo · apri prima una, poi l'altra stessa durata (460ms, rallentata): cambia solo la curva
Tendine · chiusura

E adesso escono, invece di sparire.

Prima

In chiusura la tendina svaniva di colpo: chiudere e buttare via il contenuto erano la stessa cosa, nello stesso istante, e il pannello veniva distrutto prima di poter scivolare via. Nell'ATS andava anche peggio: usciva vuota.

Dopo

Il pannello sopravvive alla chiusura quel tanto che basta per uscire di scena: 200ms sulla stessa curva con cui è entrato, contenuto ancora al suo posto.

Demo · apri, poi richiudi ciascuna durata reale: 200ms — la differenza è tutta nel secondo clic
Barra laterale

Si comprime tutta in un colpo solo.

Prima

La compressione avveniva in tre tempi: l'icona saltava al centro, il titolo di sezione lasciava uno spazio vuoto, gli item compressi restavano senza tooltip.

Dopo

Barra e contenuto si muovono all'unisono: l'icona resta ferma, il titolo si richiude in altezza, l'etichetta sfuma. E ogni item compresso mostra il suo tooltip.

Demo · clicca la barra per comprimerla barra e contenuto all'unisono, in 300ms
Il contenuto segue il bordo della barra, senza varchi né sovrapposizioni.

Se hai «riduci movimento» attivo, queste demo si adeguano da sole: resta la dissolvenza, sparisce lo spostamento. Esattamente come fa l'app.

06 · Sotto il cofano

Stack, curve di animazione e come sono implementate.

Lo stack è Solid.js, con Kobalte per le primitive dei componenti — popover, select, tooltip: accessibili, e con gli stati giusti da animare — e Motion dove una transition non basta. Ma il cuore è il vocabolario di token che rende le scelte ripetibili: i valori che senti nelle demo qui sopra sono questi.

{1}

Il vocabolario in design token. Due curve con un nome e tre durate standard. La differenza tra un'interfaccia curata e una animata a caso è tutta qui: le decisioni di motion si prendono una volta sola, poi si riusano.

DeepDive · i token del movimento CSS
Le curve e le durate, definite una volta
:root {
  /* la curva delle tendine di iOS: parte decisa, si posa morbida */
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
  /* ease-out deciso per i micro-feedback (press, hover) */
  --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);

  --dur-press: 160ms;  /* feedback al tocco */
  --dur-exit: 200ms;   /* uscite di scena */
  --dur-layout: 300ms; /* sidebar e cambi di layout */
}
{2}

Il feedback al tocco sul bottone condiviso. La proprietà scale indipendente, non transform: si compone con qualunque transform già presente sul bottone senza sovrascriverla. 160ms in entrata e in uscita: un cenno, non un salto.

DeepDive · lo stato :active CSS
Un solo componente, 162 file rifiniti
.button {
  transition:
    background-color var(--dur-press) var(--ease-out-strong),
    scale var(--dur-press) var(--ease-out-strong);
}

.button:active {
  scale: 0.97; /* la proprietà indipendente: non tocca il transform esistente */
}
{3}

L'uscita che sopravvive allo smontaggio. Un componente che si smonta alla chiusura non lascia niente da animare. Le primitive Kobalte lo gestiscono da sole: tengono vivo il nodo finché l'animazione di chiusura non è conclusa. Per i pannelli custom il pattern è questo: la chiusura diventa uno stato intermedio che tiene in vita pannello e contenuto per i 200ms dell'uscita; lo smontaggio vero arriva solo a transizione conclusa.

DeepDive · il pattern dell'uscita, ridotto all'osso CSS · TSX
Lo stato closing guida la transizione
.drawer[data-state='open'] {
  transform: translateX(0);
  transition: transform var(--dur-exit) var(--ease-drawer);
}

.drawer[data-state='closing'] {
  transform: translateX(100%); /* stessa curva dell'ingresso, al contrario */
  transition: transform var(--dur-exit) var(--ease-drawer);
}
Lo smontaggio aspetta la fine della transizione
const [state, setState] = createSignal<'open' | 'closing' | 'closed'>('open');

// chiudere non smonta: passa per lo stato intermedio
const close = () => setState('closing');

// il contenuto resta al suo posto per i 200ms dell'uscita;
// solo a transizione conclusa il pannello viene smontato davvero
const onTransitionEnd = () => {
  if (state() === 'closing') setState('closed');
};
{4}

Niente più transition-all, e il reduced motion come regola. Liste esplicite di proprietà: transform e opacity, ciò che il compositor anima senza ricalcolare il layout. E con «riduci movimento» attivo lo spostamento sparisce, la dissolvenza resta: l'informazione «qualcosa è apparso» non si perde mai.

DeepDive · proprietà giuste e reduced motion CSS
Meno movimento, non zero movimento
@media (prefers-reduced-motion: reduce) {
  .popover {
    /* la dissolvenza resta: qualcosa è apparso */
    animation: fade-in var(--dur-press) linear;
    /* lo spostamento no */
    transform: none;
  }
}
07 · Sfide

Ridisegnare la gestione dello stato e generalizzare l'intervento.

Le uscite si scontrano con il ciclo di vita dei componenti. Un pannello smontato nell'istante stesso in cui si chiude non lascia niente da animare: per questo le tendine sparivano di colpo. Per arrivare all'obiettivo ho dovuto riorganizzare la gestione dello stato — in meglio: la chiusura è diventata uno stato esplicito del componente, non un evento istantaneo, che tiene in vita pannello e contenuto per i 200ms dell'uscita; lo smontaggio arriva solo a transizione conclusa. Nell'ATS, dove la tendina si svuotava prima ancora di chiudersi, la stessa riorganizzazione ha risolto anche quello.

Generalizzare senza rompere niente. Il punto non era mai il singolo componente: era ridefinire i primitivi condivisi — bottone, popover, select, tendine — perché ogni rifinitura si propagasse da sola a tutta l'app e restasse mantenibile nel tempo. Ogni modifica è stata additiva — uno stato attivo, una curva nei token, mai un cambio di layout — e gli interventi sono stati chiusi uno alla volta, con i fix successivi emersi da review e da uso reale. Così un intervento in un punto solo ha rifinito 162 file.

08 · Prima / Dopo

Cosa è cambiato davvero.

Prima
File ed elementi con animazioni curate 0
  • Nessuno stato attivo in tutta l'app
  • Popover e menu materializzati a mezz'aria
  • Tendine distrutte all'istante, senza uscita di scena
  • transition-all ovunque e reduced motion ignorato
Dopo
File ed elementi con animazioni curate 162+
  • scale 0.97 in 160ms su ogni bottone, da un solo componente condiviso
  • Menu e popover nascono dal punto in cui li apri
  • Uscite di 200ms sulla stessa curva dell'ingresso
  • Solo transform e opacity, e chi chiede meno movimento lo ottiene

Crea un prodotto che i tuoi utenti vogliono usare.

30 minuti davanti alla tua interfaccia: ti dico onestamente dove il movimento dà informazione e dove invece manca.

Prenota una call di 30 min   Altri casi studio
Review gratuita del tuo prodotto.  Prenota 30 minuti →