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:

  1. dire al browser che vuoi transizionare anche proprietà “discrete”
  2. includere content-visibility nella 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 translate invece di transform ti 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:

  • details in colonna per far crescere bene il summary
  • 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-content semplifica enormemente styling e animazione della parte espandibile.
  • Per una transizione davvero fluida:
    • anima block-size (logico, quindi più robusto)
    • abilita interpolate-size: allow-keywords per arrivare a auto
    • aggiungi transition-behavior: allow-discrete e transiziona anche content-visibility per animare la chiusura.
  • Per micro‑interazioni più “premium”, anima gli item interni con opacity + translate e uno stagger via transition-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.