Introduzione
Le Web Components permettono di costruire elementi HTML personalizzati, autonomi e riutilizzabili in progetti diversi. In questo tutorial realizzeremo un componente chiamato app-dialog, cioè una finestra modale accessibile che può mostrare messaggi, conferme o contenuti complessi.
Una modale ben progettata non deve essere soltanto visivamente gradevole. Deve anche rispettare alcune regole importanti: consentire la chiusura con il tasto Esc, mantenere il focus all’interno della finestra mentre è aperta, fornire un nome accessibile alle tecnologie assistive e comunicare correttamente il proprio stato. Per ottenere questi risultati utilizzeremo gli standard Custom Elements, Shadow DOM e HTML Templates.
Il componente sarà configurabile tramite attributi HTML e conterrà un elemento slot, così da permettere al codice esterno di inserire liberamente testo, pulsanti o altri elementi HTML.
Codice completo
Il seguente esempio contiene il markup di utilizzo e l’implementazione JavaScript del Web Component. Il codice non richiede librerie esterne.
<button id="open-dialog">
Elimina il documento
</button>
<app-dialog
id="delete-dialog"
title="Conferma eliminazione"
labelledby="dialog-title">
<p>
Questa operazione è definitiva. Vuoi procedere?
</p>
<div slot="actions">
<button type="button" data-close>
Annulla
</button>
<button type="button" id="confirm-delete">
Elimina
</button>
</div>
</app-dialog>
<script>
class AppDialog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
:host {
display: none;
}
:host([open]) {
display: block;
}
.backdrop {
position: fixed;
inset: 0;
display: grid;
place-items: center;
background: rgb(0 0 0 / 0.6);
padding: 1rem;
}
.dialog {
width: min(100%, 32rem);
background: white;
color: #1f2937;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1rem 3rem rgb(0 0 0 / 0.25);
}
.header {
display: flex;
justify-content: space-between;
align-items: start;
gap: 1rem;
}
.close {
border: 0;
background: transparent;
font-size: 1.5rem;
cursor: pointer;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
</style>
<div class="backdrop" data-backdrop>
<section
class="dialog"
role="dialog"
aria-modal="true"
tabindex="-1"
aria-labelledby="dialog-title">
<div class="header">
<h2 id="dialog-title"></h2>
<button
class="close"
type="button"
aria-label="Chiudi finestra"
data-close>
×
</button>
</div>
<div class="content">
<slot></slot>
</div>
<div class="actions">
<slot name="actions"></slot>
</div>
</section>
</div>
`;
this.dialog = this.shadowRoot.querySelector(".dialog");
this.backdrop = this.shadowRoot.querySelector("[data-backdrop]");
this.titleElement = this.shadowRoot.querySelector("#dialog-title");
this.previousFocusedElement = null;
this.shadowRoot
.querySelectorAll("[data-close]")
.forEach(button => {
button.addEventListener("click", () => this.close());
});
this.backdrop.addEventListener("click", event => {
if (event.target === this.backdrop) {
this.close();
}
});
this.addEventListener("keydown", event => {
if (event.key === "Escape") {
this.close();
}
if (event.key === "Tab") {
this.keepFocusInside(event);
}
});
}
connectedCallback() {
this.titleElement.textContent =
this.getAttribute("title") || "Finestra di dialogo";
}
open() {
if (this.hasAttribute("open")) return;
this.previousFocusedElement = document.activeElement;
this.setAttribute("open", "");
this.dialog.focus();
this.dispatchEvent(new CustomEvent("dialog-open", {
bubbles: true
}));
}
close() {
if (!this.hasAttribute("open")) return;
this.removeAttribute("open");
if (this.previousFocusedElement) {
this.previousFocusedElement.focus();
}
this.dispatchEvent(new CustomEvent("dialog-close", {
bubbles: true
}));
}
keepFocusInside(event) {
const focusableElements = this.dialog.querySelectorAll(
"button, [href], input, select, textarea, [tabindex]:not([tabindex=´-1´])"
);
if (!focusableElements.length) return;
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
}
customElements.define("app-dialog", AppDialog);
const openButton = document.querySelector("#open-dialog");
const dialog = document.querySelector("#delete-dialog");
const confirmButton = document.querySelector("#confirm-delete");
openButton.addEventListener("click", () => {
dialog.open();
});
confirmButton.addEventListener("click", () => {
alert("Documento eliminato.");
dialog.close();
});
dialog.addEventListener("dialog-close", () => {
console.log("La modale è stata chiusa.");
});
</script> Spiegazione
Definizione del Custom Element
La classe AppDialog estende HTMLElement, la classe base degli elementi HTML. Il metodo customElements.define() associa la classe al tag app-dialog. Per convenzione, il nome di un Custom Element deve contenere almeno un trattino.
Nel costruttore viene creato uno Shadow DOM. Questo livello di incapsulamento separa struttura e stili interni del componente dal resto della pagina, riducendo il rischio di conflitti con CSS o JavaScript esterni.
Slot e contenuto personalizzabile
Lo slot principale riceve il contenuto inserito direttamente dentro app-dialog. Lo slot con nome actions riceve invece gli elementi che possiedono l’attributo slot="actions". In questo modo il componente mantiene una struttura stabile, ma resta flessibile.
Apertura, chiusura e focus
L’attributo booleano open rappresenta lo stato della modale. Il metodo open() memorizza l’elemento che aveva il focus prima dell’apertura, mostra la finestra e sposta il focus sul contenitore del dialogo. Quando la modale viene chiusa, il focus torna al pulsante originale.
La gestione del tasto Tab impedisce al focus di uscire dalla finestra. Questa tecnica è chiamata focus trap ed è particolarmente utile per chi naviga utilizzando tastiera o tecnologie assistive.
Eventi personalizzati
Gli eventi dialog-open e dialog-close permettono alla pagina di reagire ai cambiamenti del componente senza conoscere i dettagli della sua implementazione. L’opzione bubbles: true consente all’evento di propagarsi verso gli elementi genitori.
Best practice
- Usa sempre un nome descrittivo per il titolo della modale e collegalo al dialogo tramite aria-labelledby.
- Utilizza aria-modal="true" insieme a role="dialog" per comunicare correttamente la natura della finestra alle tecnologie assistive.
- Prevedi sempre una modalità di chiusura con il tasto Esc e un pulsante visibile con etichetta comprensibile.
- Evita di inserire logica specifica dell’applicazione dentro il componente. La modale dovrebbe occuparsi della presentazione e degli eventi, mentre la pagina decide cosa accade dopo una conferma.
- Non usare una modale per ogni messaggio. Per notifiche brevi o non bloccanti è spesso più adatto un componente di tipo toast.
- Testa il componente con tastiera, lettori di schermo, zoom elevato e dispositivi mobili.
- Se la modale contiene moduli o contenuti lunghi, valuta l’uso di un elemento nativo dialog con i metodi showModal() e close(), quando il supporto dei browser del progetto lo consente.
Riepilogo
Un Web Component per finestre modali centralizza struttura, comportamento e accessibilità in un elemento riutilizzabile. Grazie a Custom Elements, Shadow DOM e slot, possiamo creare un’API HTML semplice, come app-dialog, lasciando al codice esterno il controllo del contenuto e delle azioni.
L’aspetto più importante non è soltanto nascondere o mostrare un pannello, ma gestire correttamente focus, tastiera, semantica e comunicazione con la pagina. Questi principi rendono il componente più robusto e adatto a progetti reali.
Approfondisci con risorse ufficiali
- MDN Web Components: panoramica su Custom Elements, Shadow DOM e template.
- MDN CustomElementRegistry: registrazione degli elementi personalizzati.
- MDN HTMLDialogElement: riferimento per l’elemento nativo dialog.
- WAI-ARIA Authoring Practices: linee guida ufficiali per dialoghi e finestre modali accessibili.
- WHATWG HTML Standard: specifica tecnica aggiornata del linguaggio HTML.
