ITS Web Design e Strategie Digitali
ITS Academy I-CREA @ CFP Bauer

Guida interattiva a

Details, Dialog e Popover

Disclosure, modali e UI sovrapposte in HTML moderno

Tre nuovi strumenti nativi per necessità comuni

FAQ, conferme importanti, menu contestuali, tooltip e pannelli di filtri sono necessità comuni nelle interfacce web.

Per anni questi comportamenti sono stati costruiti combinando elementi generici, CSS e JavaScript. Il risultato poteva funzionare visivamente, ma lasciava allo sviluppatore la responsabilità di semantica, tastiera, focus, stato aperto o chiuso e relazione tra controllo e contenuto.

Per rendere questi pattern più solidi e accessibili, HTML mette oggi a disposizione strumenti nativi che descrivono direttamente l'intenzione dell'interfaccia:

<details>

Mostra o nasconde contenuto nella pagina, senza uscire dal flusso.

Spedizione

Gratuita sopra i 50 euro.

<dialog>

Rappresenta una finestra di dialogo, modale o non-modale.

Salvare le modifiche?

Il dialog richiede una scelta prima di continuare.

popover

Porta un elemento nel top layer per UI leggere e temporanee.

Filtri rapidi

Idea guida

Partite dal nativo quando il pattern corrisponde al problema: il browser riconosce ruolo, stato e relazioni dell'interfaccia. CSS e JavaScript restano strumenti per adattare il comportamento al progetto, non per ricostruirlo da zero ogni volta.

<details> e <summary>: disclosure nativo

Un widget di disclosure è un componente che rivela contenuto su richiesta. In italiano: un blocco espandibile.

<details> è il contenitore. <summary> è l'etichetta cliccabile. Lo usate quando il contenuto è utile, ma non deve occupare spazio o attenzione fin da subito.

<details>
  <summary>Quanto costa la spedizione?</summary>
  <p>La spedizione è gratuita sopra i 50 euro.</p>
</details>
Output renderizzato
Quanto costa la spedizione?

La spedizione è gratuita sopra i 50 euro.

Regola pratica

<summary> deve essere il primo figlio diretto di <details>. Evitate link, bottoni e input dentro al summary: rendono l'interazione ambigua.

Cosa fa il browser

  • Click, Invio e Spazio funzionano senza JavaScript.
  • Il contenuto dopo <summary> viene mostrato o nascosto.
  • Il marker, cioè la freccia all'inizio del <summary>, è generato automaticamente, ma può essere personalizzato con CSS.
  • Lo stato aperto o chiuso è esposto tramite l'attributo open.

L'attributo open

Di default un <details> parte chiuso. Se aggiungete l'attributo booleano open, parte aperto.

Mostra condizioni di reso

Potete restituire il prodotto entro 30 giorni, se integro e con confezione originale.

<details>
  <summary>Mostra condizioni di reso</summary>
  <p>Potete restituire il prodotto entro 30 giorni...</p>
</details>

Attributo booleano

open="false" non chiude il blocco. La sola presenza di open lo apre.

L'attributo name: accordion nativo

Quando più <details> condividono lo stesso name, solo uno può restare aperto alla volta.

Sviluppo web

Siti moderni, responsive e mantenibili.

Consulenza SEO

Analisi tecnica e contenuti per migliorare la presenza sui motori.

Formazione

Workshop su HTML, CSS e progettazione web.

Come usare <details>

Funziona bene per

  • FAQ e documentazione.
  • Specifiche tecniche opzionali.
  • Contenuti progressivi.
  • Opzioni avanzate.

Non è ideale per

  • Azioni critiche.
  • Menu sovrapposti.
  • Form che devono bloccare il flusso.
  • Tooltip o contenuti fluttuanti.
ErrorePerché evitarlo
Manca <summary>Il browser mostra una label generica, spesso in inglese.
<summary> non è primo figlioMarkup non valido e comportamento meno prevedibile.
Link o bottoni dentro <summary>L'utente non capisce se sta aprendo il blocco o attivando il controllo interno.
open="false"Essendo booleano, apre comunque il blocco.

CSS per <details>

::marker permette di intervenire sulla freccia nativa del <summary>, ma con proprietà limitate. Se vi serve pieno controllo visivo, nascondete il marker e ricreatelo con ::after.

<details class="faq" open>
  <summary>
    <span>Garanzia e reso</span>
  </summary>
  <p>Potete restituire il prodotto entro 30 giorni.</p>
  <p>La garanzia copre i difetti di produzione.</p>
</details>
.faq summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  list-style: none;
}

.faq summary::marker {
  content: "";
}

.faq summary::-webkit-details-marker {
  display: none; /* Safari */
}

.faq summary::after {
  content: "+";
  font-size: 1.4rem;
  line-height: 1;
}

.faq[open] summary::after {
  rotate: 45deg;
}

.faq::details-content {
  display: grid;
  gap: 0.75rem;
}
Output renderizzato
Garanzia e reso

Potete restituire il prodotto entro 30 giorni, se integro e con confezione originale.

La garanzia copre i difetti di produzione per due anni.

Nota pratica

::details-content seleziona la parte che si apre e si chiude. Evitate il padding qui: può lasciare spazio vuoto anche a blocco chiuso. È più utile per layout interni, per esempio display: grid e gap tra più paragrafi.

Provate voi!

FAQ accessibile con <details>

Aprite CodePen e costruite una FAQ con tre domande. Poi trasformatele in accordion con name.

Risultato atteso

Domande frequenti

Quanto costa la spedizione?

La spedizione standard costa 5,99 euro. Sopra i 50 euro è gratuita.

La consegna richiede di solito 2-4 giorni lavorativi.

Posso restituire un prodotto?

Sì, entro 30 giorni dall'acquisto se il prodotto è integro.

Quali metodi di pagamento accettate?

Carte, PayPal, bonifico e pagamento alla consegna.

HTML

<section class="faq">
  <h2>Domande frequenti</h2>

  <details name="faq" open>
    <summary><span>Quanto costa la spedizione?</span></summary>
    <p>La spedizione standard costa 5,99 euro. Sopra i 50 euro è gratuita.</p>
    <p>La consegna richiede di solito 2-4 giorni lavorativi.</p>
  </details>

  <details name="faq">
    <summary><span>Posso restituire un prodotto?</span></summary>
    <p>Sì, entro 30 giorni dall'acquisto se il prodotto è integro.</p>
  </details>

  <details name="faq">
    <summary><span>Quali metodi di pagamento accettate?</span></summary>
    <p>Carte, PayPal, bonifico e pagamento alla consegna.</p>
  </details>
</section>

CSS

*, *::before, *::after {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

body {
  font-family: sans-serif;
}

/* Imposto il layout generale della FAQ e la tonalità base. */
.faq {
  --hue: 32;
  --text-color: hsl(calc(var(--hue) + 0) 20% 11%);
  --muted-text-color: hsl(calc(var(--hue) + 0) 15% 29%);
  --border-color: hsl(calc(var(--hue) + 3) 24% 80%);
  --surface-color: hsl(calc(var(--hue) + 8) 50% 98%);
  --shadow-color: hsl(calc(var(--hue) - 2) 18% 10% / 0.08);
  --open-border-color: hsl(calc(var(--hue) - 3) 56% 50%);
  --summary-background-color: hsl(calc(var(--hue) + 5) 42% 93%);
  --summary-border-color: hsl(calc(var(--hue) + 2) 29% 85%);
  --focus-color: hsl(calc(var(--hue) + 1) 52% 44%);
  --marker-background-color: hsl(calc(var(--hue) + 4) 66% 94%);
  --marker-color: hsl(calc(var(--hue) - 3) 77% 31%);
  max-width: 720px;
  display: grid;
  gap: 0.75rem;
  color: var(--text-color);
}

/* Rendo il titolo più compatto rispetto al titolo della pagina. */
.faq h2 {
  margin: 0 0 0.25rem;
  font-size: 1.4rem;
}

/* Stilo ogni details come una card e taglio il contenuto agli angoli arrotondati. */
.faq details {
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background: var(--surface-color);
  box-shadow: 0 2px 8px var(--shadow-color);
  overflow: clip;
}

/* Evidenzio il blocco aperto con un bordo più caldo. */
.faq details[open] {
  border-color: var(--open-border-color);
}

/* Trasformo summary in una riga flex con testo a sinistra e marker a destra. */
.faq summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.1rem;
  cursor: pointer;
  font-weight: 700;
  list-style: none;
  color: var(--text-color);
}

/* Aggiungo feedback visivo al passaggio del mouse e al focus da tastiera. */
.faq summary:hover {
  background: var(--summary-background-color);
}

.faq summary:focus-visible {
  outline: 3px solid var(--focus-color);
  outline-offset: 3px;
}

/* Quando il details è aperto, separo il summary dal contenuto. */
.faq details[open] summary {
  background: var(--summary-background-color);
  border-bottom: 1px solid var(--summary-border-color);
}

/* Nascondo il marker nativo, incluso quello usato da Safari. */
.faq summary::marker {
  content: "";
}

.faq summary::-webkit-details-marker {
  display: none;
}

/* Ricreo il marker con un + allineato a destra. */
.faq summary::after {
  content: "+";
  width: 1.5rem;
  height: 1.5rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--border-color);
  border-radius: 999px;
  background: var(--marker-background-color);
  color: var(--marker-color);
  /* Il + di alcuni font sans-serif non risulta ben centrato. */
  font-family: monospace;
  font-size: 1.2rem;
  line-height: 1;
}

/* Ruoto il + quando il details è aperto. */
.faq details[open] summary::after {
  rotate: 45deg;
}

/* Uso ::details-content per gestire lo spazio tra più paragrafi. */
.faq details::details-content {
  display: grid;
  gap: 0.75rem;
}

/* Sistemo spaziatura e leggibilità dei paragrafi interni. */
.faq details p {
  margin: 0;
  padding: 0 1.1rem;
  color: var(--muted-text-color);
  line-height: 1.55;
}

.faq details p:first-of-type {
  padding-top: 0.85rem;
}

.faq details p:last-of-type {
  padding-bottom: 1.1rem;
}

Cosa esplorare

  • Rimuovete name="faq": cosa cambia?
  • Provate a mettere open su due blocchi dello stesso gruppo.
  • Provate la navigazione da tastiera con Tab, Spazio e Invio.
  • Aggiungete una lista o un'immagine dentro una risposta.

Sovrapposizioni: modale o non-modale?

Dialog e popover servono quando un pezzo di interfaccia deve galleggiare sopra la pagina: conferme, menu, filtri, tooltip, avvisi. Non state solo posizionando un box più in alto degli altri.

State scegliendo anche un comportamento: decidere se quel contenuto interrompe l'utente oppure se gli resta accanto mentre continua a lavorare. È questa scelta a guidare poi lo strumento giusto.

Modale

Blocca il resto della pagina. L'utente deve rispondere prima di proseguire.

Esempi: conferma di eliminazione, login obbligatorio, passaggio critico di un processo.

Non-modale

Resta sopra la pagina, ma non blocca nulla. L'utente può ignorarlo e continuare.

Esempi: tooltip, menu a tendina, pannello filtri, informazione accessoria.

Domanda progettuale

Prima di scrivere una riga di codice, chiedetevi: questo contenuto deve fermare l'utente, oppure può convivere con il resto della pagina?

Il top layer

I dialog modali e i popover usano il cosiddetto top layer: uno strato speciale gestito dal browser sopra il contenuto della pagina.

Pagina normale
header, main, footer
CSS stacking
position + z-index
Top layer del browser
dialog modali, popover, fullscreen

Questo risolve molti problemi storici di sovrapposizione: non dovete combattere con z-index: 999999.

L'elemento <dialog>

<dialog> è un elemento semantico per finestre di dialogo. Può contenere testo, immagini, liste, form e pulsanti.

<dialog id="conferma-dialog">
  <h2>Confermi l'operazione?</h2>
  <p>Questa azione non può essere annullata.</p>
  <form method="dialog">
    <button value="annulla">Annulla</button>
    <button value="conferma">Conferma</button>
  </form>
</dialog>
Output renderizzato quando il dialog è aperto

Confermi l'operazione?

Questa azione non può essere annullata.

Dialog non-modali

Un dialog non-modale può essere visibile senza bloccare il resto della pagina. L'attributo open lo mostra, ma non lo rende modale.

Esempio di finestra chat sovrapposta a una pagina di prenotazione hotel

Un pannello chat è un esempio tipico: sta sopra la pagina, ma l'utente può continuare a interagire con il resto.

<dialog open>
  <p>Ciao! Come posso aiutarvi?</p>
</dialog>
Output renderizzato

Ciao! Come posso aiutarvi?

Dialog modali con showModal()

Per aprire un dialog in modalità modale serve JavaScript minimo: il metodo showModal().

Quando il dialog è aperto, il resto della pagina è inerte e il dialog entra nel top layer.

Eliminare il file?

Questa azione non può essere annullata.

<button id="apri-dialog-modale" type="button">Apri dialog modale</button>

<dialog id="conferma-dialog-modale">
  <h3>Eliminare il file?</h3>
  <p>Questa azione non può essere annullata.</p>
  <form method="dialog">
    <button value="annulla">Annulla</button>
    <button value="elimina">Elimina</button>
  </form>
</dialog>
const bottone = document.getElementById('apri-dialog-modale');
const dialog = document.getElementById('conferma-dialog-modale');

bottone.addEventListener('click', () => {
  dialog.showModal();
});

form method="dialog"

Un form dentro un dialog può chiudere il dialog senza inviare una richiesta HTTP. Il valore del pulsante premuto viene salvato in returnValue.

Se un button non ha né valuetype, il suo comportamento predefinito resta submit: in un form con method="dialog" chiude il dialog lasciando una risposta vuota. Nella demo viene mostrata come Chiuso senza risposta, proprio come quando premete Esc.

Nessuna risposta

Salvare le modifiche?

<button id="apri-form-method-dialog" class="demo-action" type="button">Scegli una risposta</button>
<output id="form-method-dialog-output">Nessuna risposta</output>

<dialog id="form-method-dialog-modal" class="lesson-dialog">
  <h3>Salvare le modifiche?</h3>
  <form method="dialog" class="dialog-actions">
    <button>Chiudi</button>
    <button value="annulla">Annulla</button>
    <button value="salva">Salva</button>
  </form>
</dialog>
const formButton = document.getElementById('apri-form-method-dialog');
const formDialog = document.getElementById('form-method-dialog-modal');
const formOutput = document.getElementById('form-method-dialog-output');

formButton.addEventListener('click', () => {
  formDialog.returnValue = '';
  formDialog.showModal();
});

formDialog.addEventListener('close', () => {
  formOutput.value = formDialog.returnValue || 'Chiuso senza risposta';
  formOutput.textContent = formOutput.value;
});

Backdrop, focus e accessibilità

Quando un dialog viene aperto con showModal(), il browser gestisce automaticamente molti dettagli difficili.

  • Il resto della pagina diventa inerte.
  • Il dialog viene annunciato come finestra di dialogo agli screen reader.
  • Il focus entra nel dialog e torna al controllo di apertura alla chiusura.
  • Esc chiude il dialog modale.
  • ::backdrop permette di stilare lo sfondo dietro il dialog.
dialog::backdrop {
  background: rgb(0 0 0 / 0.65);
}
Output renderizzato

Dialog modale

Il contenuto dietro resta visibile, ma viene oscurato e reso inattivo.

Esempio di dialog modale per inserire un link con sfondo oscurato
Provate voi!

Dialog di conferma

Aprite CodePen e create una conferma modale per un'azione critica.

Risultato atteso

Nessuna scelta

Eliminare l'account?

Questa azione non può essere annullata.

HTML

<section class="danger-zone">
  <button id="apri" type="button">Elimina account</button>
  <output id="risultato">Nessuna scelta</output>

  <dialog id="conferma">
    <div class="dialog-content">
      <form class="dialog-close-form" method="dialog">
        <button class="dialog-close" aria-label="Chiudi dialog">×</button>
      </form>
      <h2>Eliminare l'account?</h2>
      <p>Questa azione non può essere annullata.</p>
      <form method="dialog">
        <button value="annulla">Annulla</button>
        <button value="elimina">Elimina</button>
      </form>
    </div>
  </dialog>
</section>

JavaScript

const bottone = document.getElementById('apri');
const dialog = document.getElementById('conferma');
const risultato = document.getElementById('risultato');

bottone.addEventListener('click', () => {
  dialog.returnValue = '';
  dialog.showModal();
});
dialog.addEventListener('close', () => {
  risultato.value = dialog.returnValue || 'chiuso senza scelta';
  risultato.textContent = risultato.value;
});

CSS

*, *::before, *::after {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

body {
  font-family: sans-serif;
}

/* Definisco la palette del componente partendo da una tonalità. */
.danger-zone {
  --hue: 12;
  --text-color: hsl(calc(var(--hue) + 0) 24% 12%);
  --muted-text-color: hsl(calc(var(--hue) + 0) 14% 36%);
  --surface-color: hsl(calc(var(--hue) + 12) 42% 98%);
  --panel-color: hsl(calc(var(--hue) + 8) 46% 95%);
  --border-color: hsl(calc(var(--hue) + 4) 24% 78%);
  --danger-color: hsl(calc(var(--hue) + 0) 72% 42%);
  --danger-hover-color: hsl(calc(var(--hue) + 0) 72% 35%);
  --button-text-color: hsl(calc(var(--hue) + 8) 35% 98%);
  --shadow-color: hsl(calc(var(--hue) + 0) 28% 10% / 0.18);
  --backdrop-color: hsl(calc(var(--hue) + 0) 18% 10% / 0.65);
  --focus-color: hsl(calc(var(--hue) + 8) 80% 45%);
}

/* Organizzo pulsante e output come controlli dello stesso gruppo. */
.danger-zone {
  max-width: 560px;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  color: var(--text-color);
}

/* Rendo evidente che il pulsante avvia un'azione critica. */
#apri {
  font: inherit;
  font-weight: 700;
  border: 1px solid var(--danger-color);
  border-radius: 8px;
  background: var(--danger-color);
  color: var(--button-text-color);
  padding: 0.75rem 1rem;
  cursor: pointer;
}

#apri:hover {
  background: var(--danger-hover-color);
}

/* Mostro lo stato dell'interazione vicino al pulsante. */
#risultato {
  border: 1px solid var(--border-color);
  border-radius: 999px;
  background: var(--panel-color);
  color: var(--muted-text-color);
  padding: 0.55rem 0.8rem;
}

/* Centro e stilo il dialog aperto con showModal(). */
#conferma {
  width: 420px;
  max-width: calc(100% - 2rem);
  margin: auto;
  border: 1px solid var(--border-color);
  border-radius: 10px;
  background: var(--surface-color);
  color: var(--text-color);
  box-shadow: 0 16px 48px var(--shadow-color);
  padding: 0;
  overflow: visible;
}

#conferma::backdrop {
  background: var(--backdrop-color);
}

/* Creo un contenitore interno per posizionare il pulsante di chiusura. */
#conferma .dialog-content {
  position: relative;
  padding: 1.25rem;
}

#conferma .dialog-close-form {
  margin: 0;
}

/* Sistemo gerarchia e leggibilità del contenuto. */
#conferma h2 {
  margin: 0 0 0.5rem;
  font-size: 1.35rem;
}

#conferma p {
  margin: 0;
  color: var(--muted-text-color);
  line-height: 1.45;
}

/* Allineo le azioni del dialog e distinguo primaria e secondaria. */
#conferma form {
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
  margin-top: 1.25rem;
}

#conferma button {
  font: inherit;
  font-weight: 700;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background: var(--panel-color);
  color: var(--text-color);
  padding: 0.65rem 0.9rem;
  cursor: pointer;
}

/* Ancoro il centro della X al vertice in alto a destra del dialog. */
#conferma .dialog-close {
  position: absolute;
  top: 0;
  right: 0;
  transform: translate(50%, -50%);
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--surface-color);
  padding: 0;
}

#conferma button[value="elimina"] {
  border-color: var(--danger-color);
  background: var(--danger-color);
  color: var(--button-text-color);
}

#conferma button[value="elimina"]:hover {
  background: var(--danger-hover-color);
}

/* Mantengo visibile il focus da tastiera. */
#apri:focus-visible,
#conferma button:focus-visible {
  outline: 3px solid var(--focus-color);
  outline-offset: 3px;
}

Cosa esplorare

  • Aggiungete autofocus al pulsante più sicuro.
  • Provate a chiudere con Esc.
  • Personalizzate dialog::backdrop senza animazioni.
  • Chiedetevi: questa interazione deve davvero essere modale?

L'attributo popover

popover non è un nuovo elemento: è un attributo che trasforma un elemento esistente in un contenuto sovrapposto non-modale.

Scrivere solo popover equivale a usare popover="auto": è il comportamento da scegliere per menu, dropdown e pannelli leggeri. Il browser gestisce l'apertura dal controllo collegato, la chiusura con click fuori o Esc, e chiude gli altri popover automatici quando ne aprite uno nuovo.

popover="manual" è invece una scelta esplicita: il pannello non usa il light dismiss e resta aperto finché non lo chiudete voi con un pulsante o con JavaScript. È utile per notifiche, stati persistenti o interfacce in cui la chiusura deve essere controllata.

<button popovertarget="attributo-popover-menu" class="demo-action">Apri menu</button>

<nav id="attributo-popover-menu" popover class="lesson-popover">
  <h3>Azioni</h3>
  <button>Salva</button>
  <button>Duplica</button>
  <button>Archivia</button>
</nav>

popovertargetaction

Dopo aver scelto se il popover è auto o manual, dovete decidere che cosa fa il pulsante che lo controlla.

Con il solo popovertarget, il pulsante alterna lo stato: se il popover è chiuso lo apre, se è aperto lo chiude. Con popovertargetaction potete invece rendere esplicito il comando: show apre soltanto, hide chiude soltanto, toggle alterna.

Questo è particolarmente utile con popover="manual", perché il popover non si chiude da solo: dovete prevedere un controllo chiaro per chiuderlo.

Popover manuale: non si chiude con light dismiss.

<button popovertarget="azione-popover" popovertargetaction="show" class="demo-action">Apri</button>
<button popovertarget="azione-popover" popovertargetaction="hide" class="demo-action secondary">Chiudi</button>
<button popovertarget="azione-popover" class="demo-action secondary">Alterna</button>

<div id="azione-popover" popover="manual" class="lesson-popover status-popover">
  <p>Popover manuale: non si chiude con light dismiss.</p>
</div>

La semantica non arriva da popover

popover aggiunge comportamento, ma non cambia il significato dell'elemento.

<button popovertarget="menu-semantico" class="demo-action">Apri nav</button>
<nav id="menu-semantico" popover class="lesson-popover semantic-popover-card">
  <strong><code>&lt;nav&gt;</code></strong>
  <p>Menu di navigazione o azioni.</p>
</nav>

<button popovertarget="filtri-semantici" class="demo-action">Apri aside</button>
<aside id="filtri-semantici" popover class="lesson-popover semantic-popover-card">
  <strong><code>&lt;aside&gt;</code></strong>
  <p>Pannello di filtri o opzioni.</p>
</aside>
Output renderizzato

CSS Anchor Positioning

CSS Anchor Positioning permette di collegare un elemento a un altro dichiarato come ancora, e di calcolarne la posizione rispetto a quell'ancora. Nei popover serve a risolvere un problema concreto: un elemento con attributo popover, una volta aperto, entra nel top layer, cioè il livello che il browser disegna sopra la pagina. Per default il popover viene centrato nello schermo: il legame visivo con il pulsante che lo ha aperto si perde.

Tenete separate le responsabilità: l'HTML gestisce apertura e chiusura con popover, popovertarget e popovertargetaction; il CSS decide dove collocare l'interfaccia con delle specifiche proprietà:

  • anchor-name dà un nome all'elemento che farà da punto di riferimento, per esempio il pulsante.
  • position-anchor dice all'elemento posizionato quale ancora deve seguire.
  • anchor() legge una coordinata dell'ancora, per esempio il bordo basso o il bordo sinistro.
  • anchor-size() legge una dimensione dell'ancora, utile quando il menu deve avere almeno la larghezza del pulsante.

È una soluzione moderna già utile oggi nei browser recenti, ma non ancora baseline completa: la parte base è solida per i casi semplici, mentre gestione delle collisioni, scroll container annidati, fallback complessi e interazioni più avanzate sono ancora aree da testare. Direzione probabile: stabilizzazione su Baseline nel 2026.

  • Uso pratico: dropdown, tooltip, label, callout e menu che devono allinearsi al trigger o ereditarne la larghezza.
  • Cosa evitare: non basate layout critici solo su anchor positioning senza un test multibrowser; per collisioni complesse le librerie dedicate restano più mature.

Demo completa renderizzata: un pulsante "Filtri" apre e chiude lo stesso popover. Il CSS lo aggancia al bordo inferiore del pulsante e ne replica la larghezza.

Demo completa renderizzata
Filtri rapidi

HTML

<div class="anchor-demo">
  <button class="anchor-trigger" popovertarget="filtri-menu" popovertargetaction="toggle" type="button">
    Filtri
  </button>
  <div id="filtri-menu" popover class="anchor-menu">
    <strong>Filtri rapidi</strong>
    <button>Solo disponibili</button>
    <button>In promozione</button>
    <button>Spedizione gratuita</button>
  </div>
</div>

CSS

/* Il wrapper tiene il trigger in linea con il testo della slide. */
.anchor-demo {
  display: inline-block;
}

/* Il pulsante diventa l'ancora: il nome sarà richiamato dal popover. */
.anchor-trigger {
  anchor-name: --filtri;
  width: 13rem;
  font: inherit;
  border: 2px solid #111;
  border-radius: 6px;
  background: #111;
  color: #fff;
  padding: 0.7rem 1rem;
  text-align: left;
}

/* Il popover segue l'ancora invece di restare centrato nella viewport. */
.anchor-menu {
  position: absolute;
  position-anchor: --filtri;
  /* anchor(bottom) prende il bordo basso del pulsante e aggiunge spazio. */
  top: calc(anchor(bottom) + 0.5rem);
  right: auto;
  bottom: auto;
  /* anchor(left) allinea il bordo sinistro del popover al pulsante. */
  left: anchor(left);
  margin: 0;
  width: 13rem;
  /* anchor-size(width) impedisce al menu di essere più stretto del trigger. */
  min-width: anchor-size(width);
  border: 2px solid #111;
  border-radius: 8px;
  background: #fff;
  padding: 0.85rem;
  box-shadow: 6px 6px 0 #e5e7eb;
}

/* Solo da aperto il popover diventa una griglia di opzioni. */
.anchor-menu:popover-open {
  display: grid;
  gap: 0.5rem;
  /* Safari: evita distribuzioni verticali inattese nel grid del popover. */
  align-content: start;
}

/* Stili interni: rendono leggibili titolo e opzioni del menu. */
.anchor-menu strong {
  display: block;
  font-size: 0.95rem;
}

.anchor-menu button {
  font: inherit;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  background: #f8fafc;
  padding: 0.55rem 0.65rem;
  text-align: left;
}

Popover reali: menu, tooltip, pannelli

I popover servono per interfacce leggere, temporanee e contestuali. Non devono diventare modali travestiti.

Differenze tra dialog e popover

I due strumenti finiscono entrambi sopra la pagina, ma risolvono problemi diversi. dialog nasce per finestre di dialogo strutturate; popover aggiunge un comportamento di apertura e chiusura a qualunque elemento.

Caratteristica<dialog>popover
Può essere modale?✅ Sì, con showModal().❌ No, è sempre non-modale.
Light dismiss?❌ No: nei modali c'è Esc, ma il click fuori non chiude automaticamente.✅ Sì con popover="auto".
Serve JavaScript per aprire?⚠️ Sì per il modale, perché serve showModal().✅ No: basta popovertarget.
Ha semantica nativa?✅ Sì, è una finestra di dialogo.❌ No: mantiene la semantica dell'elemento usato.
Sfondo scuro automatico?✅ Sì per i modali, tramite ::backdrop.❌ No.
Si chiude con Esc?✅ Sì nei modali.✅ Sì per i popover automatici.
Va nel top layer?✅ Sì quando è modale.✅ Sì quando è aperto.
Focus trap?✅ Sì nei modali.❌ No.
Supporta method="dialog"?✅ Sì.❌ No.

Idea chiave

Scegliete dialog quando vi serve una finestra di dialogo; scegliete popover quando vi serve un'apertura leggera, temporanea e non bloccante.

Quando usare <dialog>

Usate <dialog> quando serve una vera finestra di dialogo: l'utente deve leggere, scegliere o completare qualcosa in un contesto separato dal resto della pagina.

Serve modalità, cioè bloccare il resto della pagina

  • Conferme per azioni critiche o irreversibili.
  • Form di login o registrazione che devono essere completati.
  • Avvisi importanti che l'utente deve vedere.
  • Wizard o processi guidati multi-step.

Serve semantica da finestra di dialogo

  • Finestre di impostazioni o preferenze.
  • Pannelli informativi strutturati.
  • Qualunque "finestra nella finestra".

method="dialog" è utile quando

  • Il form deve solo chiudere il dialog.
  • Volete leggere una scelta tramite returnValue.
Output renderizzato

Conferma eliminazione

Questa azione non può essere annullata. Tutti i dati saranno persi definitivamente.

HTML

<dialog id="conferma-eliminazione" class="lesson-dialog rendered-choice-dialog" open>
  <h3>Conferma eliminazione</h3>
  <p>Questa azione non può essere annullata. Tutti i dati saranno persi definitivamente.</p>
  <form method="dialog">
    <button value="annulla">Annulla</button>
    <button value="elimina" class="danger">Sì, elimina tutto</button>
  </form>
</dialog>

La preview lo mostra aperto per leggere la struttura. In una UI reale, un dialog che deve bloccare la pagina si apre con showModal().

Quando usare popover

Usate popover per contenuti non-modali e temporanei: compaiono sopra la pagina, ma non devono bloccare il lavoro dell'utente.

Casi tipici

  • Menu contestuali e dropdown.
  • Tooltip e suggerimenti informativi.
  • Pannelli di filtri o opzioni.
  • Notifiche temporanee.
  • Selettori custom, per esempio date picker o color picker.

È una buona scelta quando volete il light dismiss

  • L'utente deve poter chiudere facilmente.
  • Click fuori significa chiudi.
  • Non è critico se il pannello viene chiuso per sbaglio.

Riduce JavaScript perché: apertura, chiusura e comportamento standard possono essere gestiti direttamente da HTML.

Output renderizzato

HTML

<button popovertarget="filtri-prodotti" class="demo-action">Filtri</button>

<aside id="filtri-prodotti" popover class="lesson-popover filter-popover">
  <h3>Filtra risultati</h3>
  <label><input type="checkbox"> Solo disponibili</label>
  <label><input type="checkbox"> Spedizione gratuita</label>
</aside>

Il pulsante apre un pannello non-modale; Esc o click fuori lo chiudono automaticamente.

Quando usarli...insieme?!

La combinazione <dialog popover> unisce il meglio dei due mondi: mantiene la semantica corretta di una finestra di dialogo, ma usa il comportamento leggero e non-modale dei popover.

Cosa ottenete

  • Semantica corretta da finestra di dialogo.
  • Light dismiss, quindi click fuori per chiudere.
  • Controllo senza JavaScript con popovertarget.
  • Top layer automatico.
  • Chiusura con Esc.
  • Sempre non-modale: non blocca la pagina.

Quando usarlo: per la maggior parte dei dialog informativi non critici, come dettagli prodotto, informazioni di servizio, note estese o contenuti strutturati che l'utente può chiudere senza conseguenze.

Output renderizzato

Informazioni dettagliate

Materiale: 100% cotone biologico.

Produzione: commercio equo certificato.

Manutenzione: lavabile a 30°.

HTML

<button popovertarget="info-prodotto-combo" class="demo-action">Dettagli prodotto</button>

<dialog id="info-prodotto-combo" popover class="lesson-dialog popover-dialog">
  <h3>Informazioni dettagliate</h3>
  <p>Materiale: 100% cotone biologico.</p>
  <p>Produzione: commercio equo certificato.</p>
  <p>Manutenzione: lavabile a 30°.</p>
  <button popovertarget="info-prodotto-combo" popovertargetaction="hide">Chiudi</button>
</dialog>

Sembra una finestra di dialogo, ma resta non-modale e si apre senza JavaScript.

Esercizio

Scheda prodotto interattiva

Costruite una scheda prodotto e-commerce partendo dal mockup Figma. Partite da una card statica e aggiungete progressivamente details, popover, dialog, CSS dedicato e JavaScript minimo. L'idea è che ogni passo aggiunga un solo "comportamento del browser" alla volta, così potete osservare cosa fa il browser da solo e cosa invece dovete chiedergli esplicitamente.

Cosa dovrà funzionare

Alla fine, la scheda dovrà comportarsi come una mini-applicazione: controlli che si aprono e si chiudono, un suggerimento contestuale, e una conferma che blocca il resto della pagina mentre l'utente decide. Concretamente:

  • tre sezioni espandibili per specifiche, confezione e recensioni;
  • un popover informativo agganciato al pulsante i accanto al prezzo;
  • una finestra di dialogo modale per confermare l'aggiunta al carrello;
  • un output che mostra il risultato della scelta.

Regole dell'esercizio

Le regole servono a tenervi onesti: ogni volta che state per scrivere JavaScript, o un div generico, fermatevi e chiedetevi se non c'è già un elemento HTML che fa quel lavoro al posto vostro.

  1. Niente JavaScript per aprire o chiudere details e popover: lo fa il browser.
  2. JavaScript solo per aprire il dialog modale e leggere returnValue.
  3. Niente <div class="dialog">: quando serve una finestra di dialogo usate <dialog>.
  4. I pulsanti senza testo chiaro, come i e ×, devono avere aria-label.
  5. La scheda è un article; le zone interne coerenti sono section con titolo.

File di partenza

Create scheda-prodotto.html, scheda-prodotto.css e scheda-prodotto.js. L'HTML iniziale è già strutturato — c'è la card, il titolo, il prezzo e il pulsante Aggiungi al carrello — ma volutamente non contiene ancora nessun details, popover o dialog: quei tre saranno il vostro lavoro nei prossimi passi. I commenti <!-- Qui andrà ... --> indicano i punti esatti in cui inserirli.

HTML di base

Prima osservate la struttura richiesta: una card prodotto con media, contenuto, pannello acquisto e area informazioni. Poi aprite il codice e copiatelo come punto di partenza.

Apri il codice HTML di base
<!doctype html>
<html lang="it">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Scheda prodotto</title>
  <link rel="stylesheet" href="scheda-prodotto.css">
</head>
<body>
  <main class="preview-shell">
    <article class="product-card" aria-labelledby="product-title">
      <div class="product-media">
        <figure>
          <img src="assets/cuffie-x2000.svg" alt="Super Cuffie Wireless X2000 nere con custodia rigida">
          <figcaption>Custodia rigida, padiglioni over-ear e autonomia pensata per una giornata intera.</figcaption>
        </figure>
      </div>

      <div class="product-content">
        <section class="product-heading">
          <p class="eyebrow">Audio personale</p>
          <h2 id="product-title">Super Cuffie Wireless X2000</h2>
          <p class="product-copy">Audio immersivo, cancellazione del rumore e comfort stabile per studio, lavoro e viaggio.</p>
        </section>

        <section class="purchase-panel" aria-labelledby="purchase-title">
          <h3 id="purchase-title">Acquisto</h3>
          <div class="price-row">
            <p class="price">149,99 €</p>
            <!-- Qui andrà il pulsante "i" che apre il popover prezzo. -->
          </div>
          <p class="product-copy">IVA inclusa, spedizione calcolata al checkout.</p>
          <div class="button-row">
            <button class="primary-button" id="aggiungi-carrello" type="button">Aggiungi al carrello</button>
          </div>
          <!-- Qui andrà l'output che riporta lo stato dell'azione. -->
        </section>

        <section class="product-info" aria-labelledby="info-title">
          <h3 id="info-title">Informazioni aggiuntive</h3>
          <div class="product-details">
            <!-- Qui andranno i tre details. -->
          </div>
        </section>
      </div>
    </article>

    <!-- Qui andrà il popover prezzo. -->
    <!-- Qui andrà il dialog di conferma carrello. -->
  </main>
  <script src="scheda-prodotto.js"></script>
</body>
</html>
Approfondimento — perché la struttura usa article e section

La scheda è un'unità di contenuto che potrebbe stare in piedi da sola in un altro contesto (lista prodotti, risultati di ricerca, pagina dedicata): è il caso d'uso classico di article. Le tre zone interne — testo introduttivo, pannello di acquisto, informazioni aggiuntive — sono parti coerenti dello stesso articolo e meritano un proprio titolo: per questo sono section con un h2 o h3 esplicito, anche quando il design non lo mostra in grande.

CSS di base

Prima di aprire lo spoiler, provate a costruire mentalmente il foglio di stile: servono una palette riutilizzabile, un reset minimo, le regole generali di pagina, stati di focus visibili, una card a due colonne, una colonna immagine stabile, una colonna contenuto in griglia, un pannello acquisto leggibile, pulsanti coerenti e un'area vuota pronta a ospitare i details. Non dovete indovinare ogni valore numerico: concentratevi sull'ordine dei gruppi e sul ruolo di ogni selettore.

Apri il codice CSS di base
/* Palette e valori condivisi della scheda. */
:root {
  color-scheme: light;
  --page: #eaf5f1;
  --surface: #f8fdfb;
  --surface-strong: #e0efea;
  --surface-soft: #f8f5eb;
  --ink: #121d1a;
  --muted: #4a5954;
  --border: #bbccc6;
  --accent: #006950;
  --accent-soft: #bceede;
  --accent-ink: #003223;
  --danger: #a13029;
  --danger-soft: #ffe2dc;
  --shadow: 0 22px 50px rgba(35, 65, 57, 0.16);
}

/* Reset minimo per rendere prevedibili spazi e dimensioni. */
*, *::before, *::after {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

/* Base tipografica e sfondo della pagina. */
body {
  min-height: 100vh;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  color: var(--ink);
  background: var(--page);
  line-height: 1.55;
}

/* I controlli ereditano il font e restano chiaramente interattivi. */
button, input { font: inherit; }
button { cursor: pointer; }

/* Il focus da tastiera deve essere visibile su bottoni e summary. */
button:focus-visible,
summary:focus-visible {
  outline: 3px solid #00a586;
  outline-offset: 3px;
}

/* Contenitore centrale della pagina di esercizio. */
.preview-shell {
  width: min(1120px, calc(100% - 32px));
  margin: 32px auto;
  display: grid;
  gap: 22px;
}

/* Card principale: due colonne, bordo, angoli e ombra. */
.product-card {
  display: grid;
  grid-template-columns: minmax(280px, 0.95fr) minmax(360px, 1.05fr);
  overflow: clip;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: var(--shadow);
}

/* Colonna immagine, separata visivamente dal contenuto. */
.product-media {
  display: grid;
  align-content: start;
  gap: 18px;
  padding: 28px;
  background: var(--surface-strong);
  border-right: 1px solid var(--border);
}

/* Figura e testo descrittivo del prodotto. */
.product-media figure { display: grid; gap: 12px; position: sticky; top: 28px; }
.product-media img { width: 100%; display: block; }
.product-media figcaption { color: var(--muted); font-size: 0.95rem; }

/* Colonna contenuto: una griglia verticale con ritmo costante. */
.product-content { display: grid; gap: 24px; padding: 30px; }
.product-heading { display: grid; gap: 10px; }

/* Etichetta sopra al titolo del prodotto. */
.eyebrow {
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Titolo e testo descrittivo della scheda. */
.product-heading h2 {
  max-width: 14ch;
  font-size: 2.25rem;
  line-height: 1.05;
  letter-spacing: 0;
}

.product-copy { max-width: 62ch; color: var(--muted); }

/* Pannello dedicato all'acquisto e al prezzo. */
.purchase-panel {
  display: grid;
  gap: 14px;
  padding: 18px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-soft);
}

/* Riga del prezzo: importo e futuro pulsante informativo. */
.price-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.price { font-size: 2rem; font-weight: 800; line-height: 1; }

/* Stile condiviso dei pulsanti principali e secondari. */
.primary-button,
.quiet-button {
  min-height: 42px;
  border-radius: 6px;
  border: 1px solid rgba(0, 0, 0, 0);
  padding: 10px 14px;
  font-weight: 750;
}

/* Varianti dei pulsanti e messaggio di stato del carrello. */
.primary-button { background: var(--accent); color: #f5faf8; }
.quiet-button { background: rgba(0, 0, 0, 0); color: var(--accent-ink); border-color: var(--border); }
.button-row { display: flex; flex-wrap: wrap; gap: 10px; }
.cart-status { min-height: 1.5em; color: var(--accent-ink); font-weight: 700; }

/* Area finale che ospiterà i pannelli details. */
.product-info { display: grid; gap: 14px; }
.product-info h3 { font-size: 1.05rem; line-height: 1.2; }
.product-details { display: grid; gap: 10px; }

Passo 1 — Informazioni progressive con details

Iniziate dalla parte più visibile: tre pannelli che si aprono e si chiudono — specifiche, confezione, recensioni. La cosa interessante è che, con <details>, il browser gestisce per voi apertura, chiusura e accessibilità da tastiera. Non dovete scrivere una riga di JavaScript.

Fermatevi un attimo prima di guardare il codice e chiedetevi: come fareste, senza JavaScript, a far sì che aprire un pannello chiuda automaticamente gli altri due? E quale dei tre dovrebbe essere già aperto al primo caricamento?

La risposta sta in due attributi nativi: name (lo stesso nome su tutti i details li trasforma in un gruppo mutuamente esclusivo, come i radio) e open (presente sull'elemento che volete trovare già aperto).

HTML da aggiungere

Scrivete tre blocchi <details> dentro .product-details: stesso name per il gruppo, open solo sul pannello che deve partire aperto, summary come primo figlio.

Apri il codice HTML dei details
<details name="info-prodotto">
  <summary><span>Specifiche tecniche</span></summary>
  <ul>
    <li>Bluetooth 5.3 con connessione multipoint.</li>
    <li>Autonomia fino a 40 ore.</li>
    <li>Ricarica rapida USB-C.</li>
  </ul>
</details>

<details name="info-prodotto" open>
  <summary><span>Contenuto della confezione</span></summary>
  <p>Dentro la confezione trovate tutto il necessario per usare subito il prodotto.</p>
  <ul>
    <li>Cuffie Wireless X2000.</li>
    <li>Cavo USB-C intrecciato da 1 metro.</li>
    <li>Custodia rigida da viaggio.</li>
    <li>Manuale rapido e card di garanzia.</li>
  </ul>
</details>

<details name="info-prodotto">
  <summary><span>Recensioni utenti</span></summary>
  <p>Gli utenti apprezzano soprattutto comfort, isolamento e autonomia. La critica più comune riguarda il tempo di ricarica completa.</p>
</details>
Dettaglio importante — l'attributo name è di fatto "accordion mode"

Senza name, ogni <details> è indipendente e l'utente potrebbe averli aperti tutti insieme. Mettendo lo stesso name su più <details>, il browser ne lascia aperto al massimo uno: aprirne un altro chiude il precedente, esattamente come ci si aspetta da un accordion. È una funzionalità relativamente recente, quindi se dovete supportare browser molto datati tenetela presente.

Poi lavorate sul CSS dei details. Prima fate sembrare ogni pannello una piccola card: bordo, raggio, sfondo e overflow che non lasci uscire gli angoli del summary. Poi trasformate il summary in una riga flex con testo a sinistra e controllo visivo a destra. Il marker nativo va nascosto sia con ::marker sia con ::-webkit-details-marker; al suo posto create un + con summary::after, dandogli dimensione fissa, forma circolare e font monospace. Quando il pannello è aperto, usate details[open] per cambiare lo stato visivo: il + ruota e il summary separa il contenuto con un bordo. Infine gestite il corpo del pannello con ::details-content e spaziatura sui figli diretti p e ul, evitando padding sul contenitore generato.

CSS da aggiungere

Apri il codice CSS dei details
/* Ogni details diventa una piccola card con angoli puliti. */
.product-details details {
  overflow: clip;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
}

/* Il pannello aperto riceve uno sfondo appena diverso. */
.product-details details[open] { background: #f2fbf8; }

/* Il summary è la riga cliccabile: testo a sinistra, marker a destra. */
.product-details summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 15px 16px;
  color: var(--ink);
  font-weight: 750;
  list-style: none;
  cursor: pointer;
}

/* Nascondiamo il marker nativo anche nei browser WebKit. */
.product-details summary::marker { content: ""; }
.product-details summary::-webkit-details-marker { display: none; }

/* Ricreiamo il marker con un + controllabile da CSS. */
.product-details summary::after {
  content: "+";
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.1rem;
  line-height: 1;
}

/* Quando il details è aperto, il + diventa una × visiva. */
.product-details details[open] summary::after { transform: rotate(45deg); }
.product-details details[open] summary { border-bottom: 1px solid var(--border); }

/* Il contenuto aperto usa grid per distanziare paragrafi e liste. */
.product-details details::details-content {
  display: grid;
  gap: 10px;
}

/* Spaziamo i figli reali, non lo pseudo-elemento details-content. */
.product-details details > p,
.product-details details > ul {
  margin-left: 16px;
  margin-right: 16px;
  color: var(--muted);
}

/* Primo e ultimo blocco ricevono margini verticali controllati. */
.product-details details > p:first-of-type,
.product-details details > ul:first-of-type { margin-top: 14px; }
.product-details details > p:last-child,
.product-details details > ul:last-child { margin-bottom: 16px; }

/* Le liste mantengono rientro e ritmo leggibile. */
.product-details ul { padding-left: 20px; }
.product-details li + li { margin-top: 4px; }
Errore comune — nascondere il marker solo con ::marker

Se scrivete solo summary::marker { content: ""; }, su Chromium tutto funziona ma su Safari il triangolino resta visibile. Per coprire entrambi serve anche la regola summary::-webkit-details-marker { display: none; }. Senza, alcuni dei vostri utenti vedranno il triangolo standard e il + personalizzato uno accanto all'altro.

Approfondimento — ruotare il + con il selettore [open]

La regola details[open] summary::after { transform: rotate(45deg); } intercetta esattamente il momento in cui l'utente apre un pannello: ruotando il + di 45° si ottiene un × visivo, e diventa subito chiaro che cliccando di nuovo si chiude il pannello. È una micro-animazione che il browser gestisce gratis, senza JavaScript.

Passo 2 — Popover del prezzo

Il prezzo della cuffia è "149,99 €", ma quel numero da solo non basta: l'utente potrebbe chiedersi se include IVA, se ci sono costi di spedizione, se il listino è quello. Una soluzione classica sarebbe un tooltip al passaggio del mouse, ma i tooltip sono cattivi: invisibili da mobile, scomodi da tastiera, problematici per chi usa screen reader. La risposta moderna è il popover nativo.

Il pattern è semplicissimo: un pulsante con popovertarget punta a un pannello con l'attributo popover. Il browser fa il resto — apertura al click, chiusura cliccando fuori o premendo Esc, gestione del top layer. Niente JavaScript.

Pulsante

Nel markup del prezzo aggiungete un pulsante piccolo: deve puntare al popover con popovertarget e avere un nome accessibile con aria-label.

Apri il codice del pulsante prezzo
<button class="price-help-button"
        popovertarget="aiuto-prezzo"
        aria-label="Mostra dettagli sul prezzo">i</button>
Errore comune — un pulsante con dentro solo "i"

Un pulsante che mostra solo la lettera i non ha un nome accessibile leggibile: chi naviga con uno screen reader sentirebbe "pulsante i", che non significa nulla. L'aria-label="Mostra dettagli sul prezzo" serve esattamente a dare al pulsante un nome comprensibile senza cambiarne l'aspetto visivo. La stessa logica varrà più tardi per la × di chiusura del dialog.

Aggiungete il popover dopo l'article, dentro main: deve essere un fratello di alto livello, non un figlio del pulsante.

Popover

Scrivete un elemento separato con id corrispondente al popovertarget, attributo popover e testo breve sul prezzo.

Apri il codice HTML del popover
<div id="aiuto-prezzo" popover class="price-popover">
  <strong>Prezzo trasparente</strong>
  <p>Il prezzo include IVA e imballaggio. Eventuali costi di spedizione vengono calcolati prima del pagamento.</p>
</div>

Per il CSS del popover partite dal pulsante i: deve essere piccolo, circolare, coerente con la palette e soprattutto deve dichiarare un anchor-name, perché sarà il punto a cui agganciare il pannello. Sul popover vero e proprio resettate il posizionamento di default del browser, usate position: absolute, collegatevi all'ancora con position-anchor, e scegliete quali lati devono combaciare con anchor(bottom) e anchor(left). Date al pannello una larghezza comoda, una larghezza minima collegata all'ancora con anchor-size(width), margine superiore, padding, bordo, sfondo scuro e ombra. Chiudete con lo stato :popover-open: quando il browser apre il popover, il pannello deve diventare una piccola griglia verticale.

CSS del popover

Apri il codice CSS del popover
/* Il pulsante informativo è piccolo, circolare e diventa l'ancora. */
.price-help-button {
  width: 30px;
  height: 30px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--accent-ink);
  font-weight: 800;
  anchor-name: --price-help;
}

/* Il popover si aggancia al pulsante e resetta il posizionamento di default. */
.price-popover {
  inset: auto;
  position: absolute;
  position-anchor: --price-help;
  top: anchor(bottom);
  left: anchor(left);
  width: 260px;
  min-width: anchor-size(width);
  margin: 8px 0 0;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--ink);
  color: #f0f7f5;
  box-shadow: 0 14px 30px rgba(8, 40, 32, 0.25);
}

/* Quando il browser lo apre, il popover diventa una griglia verticale. */
.price-popover:popover-open {
  display: grid;
  gap: 6px;
  align-content: start;
}

/* Il titolo interno ha un accento chiaro sul fondo scuro. */
.price-popover strong { color: #a9f3dd; }
Approfondimento — perché min-width: anchor-size(width)

Senza quella regola il popover sarebbe largo solo quanto il suo contenuto, che essendo poco testo finirebbe stretto e scomodo. anchor-size(width) gli dice "almeno largo quanto il pulsante che ti ha aperto". Combinato con width: 260px ottenete un popover che parte da una larghezza minima coerente con l'ancora ma può crescere se il testo è più lungo.

Dettaglio importante — :popover-open rende il popover visibile

Per default un elemento con attributo popover è nascosto (display: none). La regola .price-popover:popover-open { display: grid; } dice "quando il browser ti apre, mostrati come grid". Senza questa riga il popover non comparirà mai, anche se il pulsante sembra cliccare a vuoto.

Passo 3 — Finestra di dialogo modale

Ora arriva la parte interattiva: cliccando "Aggiungi al carrello" l'utente deve confermare la scelta. È esattamente il caso d'uso per cui esiste <dialog> — un elemento che il browser sa già rendere modale, oscurando il resto della pagina e intrappolando il focus al suo interno finché non viene chiuso.

Prima però aggiungete l'<output> sotto il pulsante. È un dettaglio semantico ma importante: output rappresenta il risultato di un'azione dell'utente, non un testo qualsiasi. Gli screen reader lo annunciano automaticamente quando il valore cambia, cosa che con un <div> dovreste gestire a mano con aria-live.

Output

Nel pannello di acquisto aggiungete un output subito dopo i pulsanti: sarà il punto in cui scriverete il risultato della scelta.

Apri il codice dell'output
<output class="cart-status" id="stato-carrello">Nessuna azione sul carrello.</output>

Aggiungete poi il dialog dopo il popover. Notate che dentro ci sono due form con method="dialog": è un trucco molto pulito per dire al browser "alla pressione di questo pulsante, chiudi la finestra e salva il value come returnValue". Niente event.preventDefault(), niente listener: solo HTML.

Dialog

Scrivete un <dialog> fuori dall'article: deve avere un titolo collegato con aria-labelledby, una × con aria-label e un form con method="dialog" per le azioni.

Apri il codice HTML del dialog
<dialog id="conferma-carrello" class="cart-dialog" aria-labelledby="cart-title">
  <div class="dialog-content">
    <form class="dialog-close-form" method="dialog">
      <button class="dialog-close" aria-label="Chiudi conferma carrello">×</button>
    </form>

    <h3 id="cart-title">Aggiungere al carrello?</h3>
    <p>State per aggiungere le Super Cuffie Wireless X2000 al carrello. Potrete modificare quantità e spedizione nel passaggio successivo.</p>
    <p class="danger-note">Questa finestra di dialogo è modale: prima di continuare dovete scegliere un'azione.</p>

    <form class="dialog-actions" method="dialog">
      <button class="quiet-button" value="annulla">Annulla</button>
      <button class="danger-button" value="aggiungi">Aggiungi</button>
    </form>
  </div>
</dialog>
Approfondimento — method="dialog" è la chiave di tutto

Un <form method="dialog"> dentro un <dialog> non invia dati a nessun server: alla submit, chiude il dialog e copia il value del pulsante che l'ha innescato dentro dialog.returnValue. È per questo che possiamo distinguere tra "Annulla" e "Aggiungi" senza JavaScript di mezzo: i due bottoni hanno value diversi e basta leggere returnValue dopo la chiusura.

Per il CSS della finestra di dialogo ragionate in quattro blocchi. Primo: il dialog deve avere una larghezza massima, colore, bordo, raggio, ombra e margin: auto per stare al centro; deve anche avere overflow: visible, perché la × sarà parzialmente fuori dal bordo. Secondo: ::backdrop deve oscurare la pagina dietro alla finestra. Terzo: il contenitore interno deve diventare una griglia con position: relative, così la form della × può essere posizionata in absolute sull'angolo alto destro e spostata con transform. Quarto: definite gli stili dei pulsanti di azione e della nota di avviso, distinguendo il bottone più pericoloso dal bottone neutro già presente nel CSS base.

CSS del dialog

Apri il codice CSS del dialog
/* La finestra di dialogo modale è centrata e lascia uscire la ×. */
.cart-dialog {
  width: min(470px, calc(100vw - 32px));
  overflow: visible;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
  padding: 0;
  margin: auto;
  box-shadow: var(--shadow);
}

/* Il backdrop oscura la pagina mentre il dialog è modale. */
.cart-dialog::backdrop { background: rgba(5, 26, 21, 0.58); }

/* Piccole regole tipografiche interne al dialog. */
.cart-dialog h3 { margin-top: 0; font-size: 1.25rem; line-height: 1.2; }
.cart-dialog p { color: var(--muted); }

/* Il contenuto crea il riferimento per posizionare la ×. */
.dialog-content {
  position: relative;
  display: grid;
  gap: 16px;
  padding: 28px;
}

/* La form della × si ancora all'angolo alto destro. */
.dialog-close-form {
  position: absolute;
  top: 0;
  right: 0;
  transform: translate(50%, -50%);
}

/* La × è un pulsante vero, circolare e ben cliccabile. */
.dialog-close {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 1.35rem;
  line-height: 1;
  box-shadow: 0 10px 20px rgba(29, 51, 45, 0.18);
}

/* Le azioni finali si allineano a destra e possono andare a capo. */
.dialog-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 2px;
}

/* Il pulsante più forte comunica l'azione principale da confermare. */
.danger-button {
  min-height: 42px;
  border: 1px solid #c5776d;
  border-radius: 6px;
  background: var(--danger);
  color: #fcf7f6;
  padding: 10px 14px;
  font-weight: 750;
}

/* La nota segnala che la finestra di dialogo richiede una scelta. */
.danger-note {
  border: 1px solid #ecb9b2;
  border-radius: 8px;
  background: var(--danger-soft);
  color: #65201b;
  padding: 12px;
  font-size: 0.95rem;
}
Dettaglio importante — ::backdrop esiste solo in modalità modale

Lo pseudo-elemento ::backdrop viene generato dal browser solo quando il dialog è stato aperto con showModal(). Se lo apriste con show() (modalità non modale), ::backdrop non esisterebbe e il resto della pagina resterebbe interagibile. Per una conferma di acquisto vogliamo che sia modale: il prossimo passo userà infatti showModal().

Approfondimento — perché overflow: visible sul dialog

La × di chiusura è posizionata in absolute con transform: translate(50%, -50%), cioè per metà fuori dal bordo del dialog. Per default i browser danno al dialog un overflow: hidden implicito che taglierebbe il pulsante a metà. overflow: visible sul dialog ripristina la possibilità di "sbordare" e fa sì che la × sia tutta visibile e cliccabile.

Passo 4 — JavaScript minimo

Il momento del JavaScript. Notate quanto poco ne serve: i details e il popover non lo richiedono affatto, e per il dialog ci servono solo due cose — aprire la finestra in modalità modale e leggere cosa l'utente ha scelto dopo la chiusura.

La struttura del codice riflette questo: un click sul pulsante apre il dialog con showModal() (resettando prima returnValue per non leggere una risposta vecchia), e un close sul dialog stesso decide cosa scrivere nell'output in base al valore restituito.

JavaScript da scrivere

Prendete i tre elementi dal DOM, aprite il dialog al click e nell'evento close aggiornate output.value in base a returnValue.

Apri il codice JavaScript
const openCartButton = document.getElementById('aggiungi-carrello');
const cartDialog = document.getElementById('conferma-carrello');
const cartStatus = document.getElementById('stato-carrello');

openCartButton.addEventListener('click', () => {
  cartDialog.returnValue = '';
  cartDialog.showModal();
});

cartDialog.addEventListener('close', () => {
  if (cartDialog.returnValue === 'aggiungi') {
    cartStatus.value = 'Prodotto aggiunto al carrello.';
    return;
  }

  if (cartDialog.returnValue === 'annulla') {
    cartStatus.value = 'Aggiunta annullata.';
    return;
  }

  cartStatus.value = 'Finestra di dialogo chiusa senza scelta.';
});
Dettaglio importante — perché resettare returnValue = '' all'apertura

Se l'utente apre il dialog, sceglie "Aggiungi", lo richiude e poi lo riapre, returnValue conterrebbe ancora la stringa 'aggiungi' dalla volta precedente. Se questa volta l'utente chiudesse il dialog premendo Esc (cioè senza scegliere niente), il vostro codice vedrebbe ancora 'aggiungi' e mostrerebbe il messaggio sbagliato. Resettare returnValue a stringa vuota all'apertura risolve il problema in una riga.

Approfondimento — l'evento close intercetta tutte le chiusure

Il dialog può chiudersi in tanti modi: pulsante "Annulla", pulsante "Aggiungi", Esc da tastiera, click sulla ×. Tutti questi scenari emettono lo stesso evento close, e si distinguono solo dal returnValue. Per questo nel codice ci sono tre rami: 'aggiungi', 'annulla', e tutto il resto (Esc o ×) trattato come "chiusura senza scelta".

Checklist finale

Quando avete finito, ripercorrete questa lista a freddo: ogni punto deve funzionare senza che voi tocchiate la tastiera del codice. Se uno fallisce, il problema è quasi sempre nei punti 4-5 (accessibilità) o nel reset di returnValue.

  1. I tre details si aprono uno alla volta.
  2. Il pulsante i apre il popover scuro e il click fuori lo chiude.
  3. Aggiungi al carrello apre una finestra di dialogo modale con sfondo oscurato.
  4. Esc o × chiudono senza scelta e aggiornano l'output.
  5. Annulla e Aggiungi aggiornano l'output con messaggi diversi.
  6. Il focus resta nella finestra mentre il dialog modale è aperto.
  7. Tutti i controlli hanno nome accessibile comprensibile.

Grazie

Il browser sa fare più cose di quanto sembri.

Prossimo passo: usare queste primitive dentro componenti reali, senza perdere semantica e accessibilità.

1 / 27
Indice slide · ⌘K

Indice slide

⌘K