Guida interattiva a
Disclosure, modali e UI sovrapposte in HTML moderno
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.
Gratuita sopra i 50 euro.
<dialog>Rappresenta una finestra di dialogo, modale o non-modale.
popoverPorta un elemento nel top layer per UI leggere e temporanee.
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 nativoUn 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>
La spedizione è gratuita sopra i 50 euro.
<summary> deve essere il primo figlio diretto di <details>. Evitate link, bottoni e input dentro al summary: rendono l'interazione ambigua.
<summary> viene mostrato o nascosto.<summary>, è generato automaticamente, ma può essere personalizzato con CSS.open.openDi default un <details> parte chiuso. Se aggiungete l'attributo booleano open, parte aperto.
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>
open="false" non chiude il blocco. La sola presenza di open lo apre.
name: accordion nativoQuando più <details> condividono lo stesso name, solo uno può restare aperto alla volta.
Siti moderni, responsive e mantenibili.
Analisi tecnica e contenuti per migliorare la presenza sui motori.
Workshop su HTML, CSS e progettazione web.
<details>| Errore | Perché evitarlo |
|---|---|
Manca <summary> | Il browser mostra una label generica, spesso in inglese. |
<summary> non è primo figlio | Markup 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. |
<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;
}
Potete restituire il prodotto entro 30 giorni, se integro e con confezione originale.
La garanzia copre i difetti di produzione per due anni.
::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.
<details>Aprite CodePen e costruite una FAQ con tre domande. Poi trasformatele in accordion con name.
La spedizione standard costa 5,99 euro. Sopra i 50 euro è gratuita.
La consegna richiede di solito 2-4 giorni lavorativi.
Sì, entro 30 giorni dall'acquisto se il prodotto è integro.
Carte, PayPal, bonifico e pagamento alla consegna.
<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>
*, *::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;
}
name="faq": cosa cambia?open su due blocchi dello stesso gruppo.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.
Blocca il resto della pagina. L'utente deve rispondere prima di proseguire.
Esempi: conferma di eliminazione, login obbligatorio, passaggio critico di un processo.
Resta sopra la pagina, ma non blocca nulla. L'utente può ignorarlo e continuare.
Esempi: tooltip, menu a tendina, pannello filtri, informazione accessoria.
Prima di scrivere una riga di codice, chiedetevi: questo contenuto deve fermare l'utente, oppure può convivere con il resto della pagina?
I dialog modali e i popover usano il cosiddetto top layer: uno strato speciale gestito dal browser sopra il contenuto della pagina.
header, main, footerposition + z-indexdialog modali, popover, fullscreenQuesto risolve molti problemi storici di sovrapposizione: non dovete combattere con z-index: 999999.
<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>
Questa azione non può essere annullata.
Un dialog non-modale può essere visibile senza bloccare il resto della pagina. L'attributo open lo mostra, ma non lo rende modale.
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>
Ciao! Come posso aiutarvi?
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.
<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é value né type, 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.
<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;
});
Quando un dialog viene aperto con showModal(), il browser gestisce automaticamente molti dettagli difficili.
dialog viene annunciato come finestra di dialogo agli screen reader.Esc chiude il dialog modale.::backdrop permette di stilare lo sfondo dietro il dialog.dialog::backdrop {
background: rgb(0 0 0 / 0.65);
}
Il contenuto dietro resta visibile, ma viene oscurato e reso inattivo.
Aprite CodePen e create una conferma modale per un'azione critica.
<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>
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;
});
*, *::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;
}
autofocus al pulsante più sicuro.Esc.dialog::backdrop senza animazioni.popoverpopover 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>
popovertargetactionDopo 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>
popoverpopover 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><nav></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><aside></code></strong>
<p>Pannello di filtri o opzioni.</p>
</aside>
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.
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.
<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>
/* 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;
}
I popover servono per interfacce leggere, temporanee e contestuali. Non devono diventare modali travestiti.
dialog e popoverI 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. |
Scegliete dialog quando vi serve una finestra di dialogo; scegliete popover quando vi serve un'apertura leggera, temporanea e non bloccante.
<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.
method="dialog" è utile quandoreturnValue.<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().
popoverUsate popover per contenuti non-modali e temporanei: compaiono sopra la pagina, ma non devono bloccare il lavoro dell'utente.
Riduce JavaScript perché: apertura, chiusura e comportamento standard possono essere gestiti direttamente da 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.
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.
popovertarget.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.
<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.
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.
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:
i accanto al prezzo;output che mostra il risultato della scelta.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.
details e popover: lo fa il browser.dialog modale e leggere returnValue.<div class="dialog">: quando serve una finestra di dialogo usate <dialog>.i e ×, devono avere aria-label.article; le zone interne coerenti sono section con titolo.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.
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.
<!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>
article e sectionLa 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.
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.
/* 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; }
detailsIniziate 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).
Scrivete tre blocchi <details> dentro .product-details: stesso name per il gruppo, open solo sul pannello che deve partire aperto, summary come primo figlio.
<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>
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.
/* 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; }
::markerSe 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.
+ 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.
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.
Nel markup del prezzo aggiungete un pulsante piccolo: deve puntare al popover con popovertarget e avere un nome accessibile con aria-label.
<button class="price-help-button"
popovertarget="aiuto-prezzo"
aria-label="Mostra dettagli sul prezzo">i</button>
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.
Scrivete un elemento separato con id corrispondente al popovertarget, attributo popover e testo breve sul prezzo.
<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.
/* 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; }
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.
:popover-open rende il popover visibilePer 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.
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.
Nel pannello di acquisto aggiungete un output subito dopo i pulsanti: sarà il punto in cui scriverete il risultato della scelta.
<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.
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.
<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>
method="dialog" è la chiave di tuttoUn <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.
/* 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;
}
::backdrop esiste solo in modalità modaleLo 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().
overflow: visible sul dialogLa × 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.
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.
Prendete i tre elementi dal DOM, aprite il dialog al click e nell'evento close aggiornate output.value in base a returnValue.
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.';
});
returnValue = '' all'aperturaSe 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.
close intercetta tutte le chiusureIl 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".
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.
details si aprono uno alla volta.i apre il popover scuro e il click fuori lo chiude.Aggiungi al carrello apre una finestra di dialogo modale con sfondo oscurato.Esc o × chiudono senza scelta e aggiornano l'output.Annulla e Aggiungi aggiornano l'output con messaggi diversi.dialog modale è aperto.Il browser sa fare più cose di quanto sembri.
Prossimo passo: usare queste primitive dentro componenti reali, senza perdere semantica e accessibilità.