Site icon

Dialogo nel DOM: modali accessibili, <dialog> e best practice

Dialogo nel DOM: modali accessibili, e best practice

Dialogo nel DOM: modali accessibili, e best practice

In breve: il “dialogo” nel DOM è un componente UI che richiede attenzione a accessibilità, controllo del focus e interazione da tastiera. Oggi possiamo usare l’elemento nativo <dialog>, oppure implementare modali custom con ARIA. In questa guida vediamo quando usarlo, come farlo bene e cosa evitare, con esempi step-by-step.

Cos’è un dialogo nel DOM e quando usarlo

Un dialogo (o modale) è un pannello che si sovrappone alla pagina per richiedere un’azione o fornire informazioni contestuali. Va usato con parsimonia: è ideale per conferme, form brevi, scelte critiche o interruzioni intenzionali.
Non è adatto per contenuti lunghi o navigazione complessa (in quel caso, meglio una pagina dedicata).

Opzione 1: usare l’elemento nativo <dialog>

<dialog> è un elemento HTML che fornisce un comportamento nativo per finestre di dialogo. Offre i metodi show(), showModal() e close(), oltre all’attributo open e all’evento close.

Esempio base con <dialog>

<button id="openDialog">Apri dialog</button>

<dialog id="myDialog">
  <form method="dialog">
    <h2>Conferma azione</h2>
    <p>Vuoi davvero procedere?</p>
    <menu>
      <button value="cancel">Annulla</button>
      <button value="ok">Conferma</button>
    </menu>
  </form>
</dialog>

<script>
  const dlg = document.getElementById('myDialog');
  document.getElementById('openDialog').addEventListener('click', () => {
    dlg.showModal(); // show() non è modale; showModal() sì
  });
  dlg.addEventListener('close', () => {
    console.log('Dialog chiuso con valore:', dlg.returnValue);
  });
</script>

Con method="dialog" il pulsante invia il valore a returnValue e chiude il dialogo.
Vantaggi: API semplici, focus e scroll lock gestiti in gran parte dal browser.
Attenzione: verifica la compatibilità dei browser target; per ambienti legacy valuta un polyfill.

Stilizzare un <dialog>

dialog::backdrop { 
  background: rgba(0,0,0,.45); 
}
dialog {
  border: 0;
  border-radius: 12px;
  padding: 20px;
  max-width: 520px;
  width: 90%;
  box-shadow: 0 20px 60px rgba(0,0,0,.2);
}

Il ::backdrop controlla lo sfondo scurito. Ricorda contrasto sufficiente e dimensioni responsive.

Opzione 2: modale custom con ARIA (quando non puoi usare <dialog>)

Se non puoi usare <dialog>, crea una modale con markup semantico e ruoli ARIA.
Gli ingredienti chiave sono:

Markup ARIA consigliato

<button id="open">Apri impostazioni</button>

<div class="backdrop" hidden></div>
<div class="modal" role="dialog" aria-modal="true" 
     aria-labelledby="modal-title" aria-describedby="modal-desc" hidden>
  <h2 id="modal-title">Impostazioni account</h2>
  <p id="modal-desc">Aggiorna email e preferenze.</p>
  <form>
    <label>Email<br><input type="email"></label>
    <div class="actions">
      <button type="button" id="cancel">Annulla</button>
      <button type="submit">Salva</button>
    </div>
  </form>
</div>

Focus trap e tastiera

const openBtn = document.getElementById('open');
const modal = document.querySelector('.modal');
const backdrop = document.querySelector('.backdrop');
let lastFocused = null;

function openModal() {
  lastFocused = document.activeElement;
  backdrop.hidden = false;
  modal.hidden = false;
  const focusables = modal.querySelectorAll('a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])');
  (focusables[0] || modal).focus();

  function onKeydown(e){
    if (e.key === 'Escape'){ closeModal(); }
    if (e.key === 'Tab'){
      const list = Array.from(focusables);
      const i = list.indexOf(document.activeElement);
      if (e.shiftKey && (i === 0 || i === -1)) { e.preventDefault(); list[list.length - 1].focus(); }
      else if (!e.shiftKey && i === list.length - 1){ e.preventDefault(); list[0].focus(); }
    }
  }
  modal.addEventListener('keydown', onKeydown);
  modal._cleanup = () => modal.removeEventListener('keydown', onKeydown);
}

function closeModal(){
  backdrop.hidden = true;
  modal.hidden = true;
  modal._cleanup && modal._cleanup();
  lastFocused && lastFocused.focus();
}

openBtn.addEventListener('click', openModal);
document.getElementById('cancel').addEventListener('click', closeModal);
backdrop.addEventListener('click', closeModal);

Questa logica impedisce di “uscire” dal dialogo con Tab, chiude con Esc, e ripristina il focus al punto di origine. È un requisito di accessibilità fondamentale.

Accessibilità: checklist essenziale

Animazioni e micro-interazioni

Le animazioni migliorano la percezione: usa fade/scale brevi (150–250ms). Evita rimbalzi eccessivi. Rispetta la preferenza utente prefers-reduced-motion.

.backdrop{position:fixed;inset:0;background:rgba(0,0,0,.46);opacity:0;transition:.2s}
.modal{position:fixed;inset:auto;left:50%;top:50%;transform:translate(-50%,-50%) scale(.98);
       background:#fff;border-radius:12px;min-width:280px;max-width:560px;opacity:0;transition:.2s}
.backdrop[hidden], .modal[hidden]{display:none}
.backdrop.show{opacity:1}
.modal.show{opacity:1;transform:translate(-50%,-50%) scale(1)}
@media (prefers-reduced-motion: reduce){
  .backdrop,.modal{transition:none}
}

SEO e performance: cosa conta davvero

Pattern comuni e anti-pattern

Buoni pattern

Da evitare

Confronto rapido: <dialog> vs ARIA custom

Caratteristica <dialog> Custom ARIA
API Nativa (show/showModal/close) JS personalizzato
Focus/scroll Gestione nativa decente Da implementare manualmente
Compatibilità Buona, ma verifica target Totale, ma più lavoro
Tempo sviluppo Minore Maggiore

Esempio completo: dialog di conferma con <dialog> + animazione CSS

<button id="danger">Elimina elemento</button>

<dialog id="confirm">
  <h3>Sei sicuro?</h3>
  <p>Questa azione non è reversibile.</p>
  <form method="dialog">
    <menu>
      <button value="cancel">Annulla</button>
      <button value="ok" class="danger">Elimina</button>
    </menu>
  </form>
</dialog>

<style>
  dialog{opacity:0;transform:translateY(8px);transition:.18s}
  dialog[open]{opacity:1;transform:translateY(0)}
  dialog::backdrop{background:rgba(0,0,0,.45)}
  .danger{background:#e11d48;color:#fff;border:0;padding:.6rem 1rem;border-radius:8px}
</style>

<script>
  const d = document.getElementById('confirm');
  document.getElementById('danger').addEventListener('click', () => d.showModal());
  d.addEventListener('close', () => {
    if (d.returnValue === 'ok') {
      // esegui l’azione distruttiva
      console.log('Elemento eliminato');
    }
  });
</script>

Errori comuni (e come risolverli)

  1. Dimenticare il focus: senza focus trap, gli screen reader e gli utenti da tastiera “perdono” il contesto. Implementa sempre la gestione di Tab.
  2. Assenza di etichetta: mancano aria-labelledby/aria-describedby. Associa un titolo semanticamente corretto.
  3. Chiusura non ovvia: inserisci un pulsante “Chiudi” chiaro, oltre a supportare Esc.
  4. Contenuti critici solo in modale: prevedi una pagina alternativa indicizzabile.
  5. Backdrop non cliccabile: se la UX lo consente, permetti di chiudere cliccando l’overlay (ma mantieni anche un pulsante dedicato).

Conclusioni

I dialoghi nel DOM richiedono cura: che tu scelga <dialog> o una soluzione ARIA custom, il traguardo è una modale accessibile, chiara e performante. Parti dai pattern base qui sopra, adatta stile e interazioni al tuo progetto e testa sempre con tastiera e screen reader.

Exit mobile version