30 settembre 2026
Animare <details> e <summary> in CSS: card “accordion” fluide, senza JavaScript
Dal semplice toggle nativo a una micro‑interazione completa: transizioni su altezza auto, gestione di content-visibility e comparsa “staggered” degli elementi.
Usare <details> e <summary> per rivelare contenuti è una scelta elegante: accessibile, nativa e spesso sufficiente senza JavaScript. Con alcune proprietà CSS moderne puoi trasformare questo pattern in una micro‑interazione curata: apertura/chiusura fluida, transizione anche in chiusura (non scontata) e animazioni progressive sugli elementi interni. Vediamo come impostarlo in modo pulito, con progressive enhancement ragionato.
Perché usare <details> per una card “reveal”
Quando devi mostrare/nascondere informazioni aggiuntive (stat, note, metadati, ecc.), <details> ti regala gratuitamente:
- interazione nativa (toggle click/tap)
- semantica sensata (un contenuto espandibile)
- accessibilità di base senza dover reinventare ARIA e gestione focus
In più puoi ottenere un comportamento “accordion” senza JS: se più elementi <details> condividono lo stesso name, solo uno resta aperto alla volta.
<details class="card" name="drivers">
<summary class="card__summary">
<span class="card__title">Oscar P.</span>
<span class="card__team">McLaren</span>
<span class="card__pos">P1</span>
</summary>
<div class="card__extra">
<!-- contenuto extra -->
</div>
</details>
<details class="card" name="drivers">
...
</details>
Nota pratica: cosa mettere dentro summary
Dentro summary conviene usare elementi “testuali” (es. span). In molti browser il contenuto consentito è più restrittivo rispetto al resto del details. Fuori dal summary puoi usare tranquillamente div, grid, ecc.
Selezionare “tutto ciò che non è summary”: ::details-content
Il nodo che contiene ciò che sta dopo summary non è sempre comodo da prendere di mira, perché <details> ha un comportamento interno un po’ speciale.
La soluzione moderna è usare il pseudo-elemento:
.card::details-content {
/* tutto ciò che è dentro <details> escluso <summary> */
}
È ben supportato nei motori principali (vale sempre la pena verificare i target del tuo progetto), e soprattutto rende l’intento chiarissimo.
Animare l’apertura: da block-size: 0 a auto
L’idea è semplice: chiuso = altezza zero, aperto = altezza naturale.
.card::details-content {
overflow: hidden;
block-size: 0;
transition: block-size 400ms;
}
.card[open]::details-content {
block-size: auto;
}
Peccato che, storicamente, non puoi transizionare verso auto. Oggi esiste un’opzione dedicata.
Abilitare le keyword intrinseche: interpolate-size: allow-keywords
Questa dichiarazione (opt-in) abilita l’interpolazione verso grandezze intrinseche come auto:
html {
interpolate-size: allow-keywords;
}
- È perfetta come progressive enhancement.
- Al momento non è uniforme su tutti i browser: se non c’è supporto, il comportamento torna “a scatto” (ma resta usabile).
Se vuoi una alternativa cross-browser, esistono workaround (es. grid “collapsing”), ma qui restiamo sul percorso moderno.
Il problema della chiusura: content-visibility e transizioni “discrete”
Potresti notare un comportamento fastidioso:
- l’apertura transiziona,
- la chiusura invece sparisce senza animazione.
Questo succede perché il contenuto di <details> in chiusura viene gestito con una logica simile a una rimozione istantanea: entra in gioco content-visibility.
Per far sì che anche la chiusura rispetti il tempo di transizione, puoi:
- dire al browser che vuoi transizionare anche proprietà “discrete”
- includere
content-visibilitynella transizione
.card::details-content {
overflow: hidden;
block-size: 0;
transition-property: block-size, content-visibility;
transition-duration: 400ms;
/* chiave per far “aspettare” lo switch discreto */
transition-behavior: allow-discrete;
}
.card[open]::details-content {
block-size: auto;
}
Risultato: apertura e chiusura diventano entrambe fluide.
Dare “vita” ai contenuti: fade + slide con stagger
A questo punto l’accordion funziona, ma puoi renderlo più raffinato animando gli elementi interni quando il pannello si apre.
Immagina una lista di statistiche, ognuna composta da valore + label:
<div class="stats">
<p class="stat" style="--i: 0">
<span class="stat__value">25</span>
<span class="stat__label">Punti</span>
</p>
<p class="stat" style="--i: 1">
<span class="stat__value">1</span>
<span class="stat__label">Vittorie</span>
</p>
<p class="stat" style="--i: 2">...</p>
</div>
Stato “chiuso”: elementi abbassati e trasparenti
.stat__value,
.stat__label {
opacity: 0;
translate: 0 40%;
transition-property: translate, opacity;
transition-duration: 300ms;
}
Stato “aperto”: rientrano e diventano visibili, con ritardo progressivo
.card[open] .stat__value,
.card[open] .stat__label {
opacity: 1;
translate: 0 0;
transition-delay: calc(var(--i) * 80ms);
}
Due dettagli che fanno la differenza:
- Usare
translateinvece ditransformti mantiene nel filone delle proprietà moderne e spesso più leggibili. - Usare una percentuale (
40%) produce un effetto leggermente diverso tra elementi con altezza diversa (valori grandi vs label piccole), dando una sensazione più organica.
L’effetto “non anima al primo click”: @starting-style
Con <details>, il contenuto può non essere “pronto” per una transizione al primo toggle, perché fino all’apertura alcuni stili/box non sono realmente calcolati come te li aspetti.
Puoi forzare uno stato iniziale per la prima volta usando @starting-style:
.card[open] .stat__value,
.card[open] .stat__label {
opacity: 1;
translate: 0 0;
transition-delay: calc(var(--i) * 80ms);
}
@starting-style {
.card[open] .stat__value,
.card[open] .stat__label {
opacity: 0;
translate: 0 40%;
}
}
Così l’animazione parte correttamente anche alla prima apertura.
Layout: flex e grid (solo il necessario)
Due scelte tipiche per una card di questo tipo:
detailsin colonna per far crescere bene ilsummary- griglia (o flex) per affiancare le statistiche
Esempio essenziale:
.card {
display: flex;
flex-direction: column;
}
.card__summary {
display: flex;
gap: 1rem;
}
.stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: .75rem;
}
Sintesi operativa
<details>+<summary>è un pattern solido per rivelare contenuti senza JS.::details-contentsemplifica enormemente styling e animazione della parte espandibile.- Per una transizione davvero fluida:
- anima
block-size(logico, quindi più robusto) - abilita
interpolate-size: allow-keywordsper arrivare aauto - aggiungi
transition-behavior: allow-discretee transiziona anchecontent-visibilityper animare la chiusura.
- anima
- Per micro‑interazioni più “premium”, anima gli item interni con
opacity+translatee uno stagger viatransition-delay.
Curare questi dettagli non serve solo a “fare scena”: rende la UI più leggibile, guida l’occhio dell’utente e dà la sensazione di un’interfaccia pensata—pur restando su una soluzione leggera, semantica e manutenibile.