W3docs

JavaScript Shadow DOM

Lo Shadow DOM consente di allegare un albero DOM incapsulato a un elemento, isolando markup, stili e script dal resto della pagina. Scopri shadow root, slot e componenti riutilizzabili.

Lo Shadow DOM è un elemento fondamentale dei Web Components e consente di allegare un albero DOM incapsulato con un ambito di stile isolato a un elemento. Questa guida illustra cos'è lo Shadow DOM, perché è importante, come creare shadow root aperte e chiuse, come applicare stili con scope, come proiettare contenuti tramite slot e come collegare tutto all'interno di un elemento personalizzato riutilizzabile.

Cos'è lo Shadow DOM?

Lo Shadow DOM permette di collegare un sottoalbero DOM separato e nascosto a un elemento. Il markup e gli stili all'interno di quel sottoalbero sono incapsulati: non fuoriescono verso l'esterno e gli stili globali non entrano dall'esterno. Questo risolve uno dei problemi più antichi nello sviluppo front-end: la collisione di CSS globale e di ID tra componenti diversi.

Vale la pena definire alcuni termini fondamentali:

  • Shadow host — l'elemento normale a cui è collegato l'albero shadow.
  • Shadow root — il nodo radice dell'albero nascosto, restituito da attachShadow().
  • Shadow tree — il DOM all'interno della shadow root.
  • Light DOM — i figli ordinari dell'elemento, scritti nel markup normale; possono essere proiettati nell'albero shadow tramite gli slot.

Il browser stesso utilizza internamente lo Shadow DOM: i controlli di un elemento <video> o <input type="range"> vivono in un albero shadow irraggiungibile dall'esterno, ed è proprio per questo che i loro dettagli interni non entrano mai in conflitto con il tuo CSS.

Nell'esempio seguente, due elementi condividono la classe shadow-box, ma ciascuno mantiene il proprio stile perché uno si trova nel documento principale e l'altro all'interno di una shadow root.

<head>
  <style>
    .shadow-box {
      padding: 10px;
      border: 1px solid #000;
      background-color: lightcoral;
      color: white;
    }
  </style>
</head>
<body>
  <div class="shadow-box">This is styled by the main document</div>
  <div id="host"></div>
  <script>
    // Create a shadow root
    const hostElement = document.getElementById('host');
    const shadowRoot = hostElement.attachShadow({ mode: 'open' });

    // Attach shadow DOM content
    shadowRoot.innerHTML = `
      <style>
        .shadow-box {
          padding: 10px;
          border: 1px solid #000;
          background-color: lightblue;
          color: black;
        }
      </style>
      <div class="shadow-box">Hello, Shadow DOM!</div>
    `;
  </script>
</body>

In questo esempio ci sono due elementi con lo stesso nome di classe shadow-box. Il primo elemento è stilizzato dal CSS del documento principale, mentre il secondo è stilizzato dal CSS dello Shadow DOM. Come si può notare, gli stili definiti nello Shadow DOM non influenzano gli elementi del documento principale e viceversa. Ciò dimostra l'incapsulamento offerto dallo Shadow DOM, che consente di creare componenti isolati e riutilizzabili senza preoccuparsi dei conflitti di stile.

Creare una Shadow Root

Per creare una shadow root, si utilizza il metodo attachShadow su un elemento. La shadow root può essere open o closed. Una shadow root open è accessibile da JavaScript esterno all'albero shadow, mentre una shadow root closed non lo è.

Shadow Root Aperta

Una shadow root aperta consente l'accesso e la manipolazione da parte di JavaScript esterno. Nell'esempio seguente, manipoliamo il contenuto testuale all'interno della shadow root dopo che essa è stata creata.

<body>
  <div id="open-shadow-host"></div>
  <button id="open-shadow-btn">Change Shadow Content</button>

  <script>
    const openShadowHost = document.getElementById('open-shadow-host');
    const openShadowRoot = openShadowHost.attachShadow({ mode: 'open' });

    openShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: blue;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is an open shadow root</div>
    `;

    document.getElementById('open-shadow-btn').addEventListener('click', () => {
      openShadowRoot.querySelector('.shadow-content').textContent = 'Open Shadow Root content updated!';
    });
  </script>
</body>

In questo esempio viene fornito un pulsante per modificare il contenuto dello Shadow DOM. Poiché la shadow root è aperta, possiamo accedere al suo contenuto e manipolarlo dal documento principale.

Shadow Root Chiusa

Una shadow root chiusa limita l'accesso da parte di script esterni, garantendo un maggiore incapsulamento. Nell'esempio seguente, tentiamo di manipolare il contenuto testuale all'interno della shadow root dopo che è stata creata, ma non è possibile poiché è closed.

<body>
  <div id="closed-shadow-host"></div>
  <button id="closed-shadow-btn">Try to Change Shadow Content</button>

  <script>
    const closedShadowHost = document.getElementById('closed-shadow-host');
    const closedShadowRoot = closedShadowHost.attachShadow({ mode: 'closed' });

    closedShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: red;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is a closed shadow root</div>
    `;

    // closedShadowHost.shadowRoot is null for closed roots, so this throws a TypeError
    document.getElementById('closed-shadow-btn').addEventListener('click', () => {
      try {
        closedShadowHost.shadowRoot.querySelector('.shadow-content').textContent = 'Attempted to update closed shadow root!';
      } catch (e) {
        alert('Cannot access shadow root content from outside!');
      }
    });
  </script>
</body>

Qui il tentativo fallisce perché la shadow root è chiusa: closedShadowHost.shadowRoot restituisce null, quindi null.querySelector(...) lancia un TypeError e viene eseguito il blocco catch. Il riferimento restituito da attachShadow({ mode: 'closed' }) è l'unico modo per raggiungere quell'albero, quindi tienilo privato all'interno del tuo componente.

Un malinteso comune è che closed renda un componente davvero sicuro — non è così. Impedisce soltanto l'accesso esterno occasionale; il codice che possiede il riferimento originale alla root (o che sovrascrive attachShadow) può comunque accedervi. Usa open a meno che tu non abbia una ragione concreta per nascondere gli interni, perché open rende il debug e i test molto più semplici.

Aspettomode: 'open'mode: 'closed'
host.shadowRootRestituisce la shadow rootRestituisce null
Accesso esternoConsentito tramite host.shadowRootSolo tramite il riferimento salvato
Uso tipicoLa maggior parte dei componenti, debug facileNascondere gli interni agli script della pagina
Ispezione DevToolsCompletamente visibileVisibile, ma più difficile da gestire via script

Stili all'interno dello Shadow DOM

Attenzione

Quando si implementa JavaScript Shadow DOM, è fondamentale garantire un corretto incapsulamento per prevenire conflitti indesiderati di stile o di scripting.

Gli stili definiti all'interno di una shadow root non influenzano gli elementi esterni ad essa, e viceversa. Questo incapsulamento è utile per creare componenti riutilizzabili.

<head>
  <style>
    .styled-box {
      color: red;
      background-color: yellow;
      padding: 10px;
      border: 1px solid green;
    }
  </style>
</head>
<body>
  <div class="styled-box">This is styled by the main document</div>
  <div id="styled-host"></div>

  <script>
    const styledHost = document.getElementById('styled-host');
    const shadowRoot = styledHost.attachShadow({ mode: 'open' });

    shadowRoot.innerHTML = `
      <style>
        .styled-box {
          color: white;
          background-color: black;
          padding: 10px;
          border-radius: 5px;
        }
      </style>
      <div class="styled-box">Styled by Shadow DOM</div>
    `;
  </script>
</body>

In questo esempio ci sono due elementi con il nome di classe styled-box. Il primo elemento è stilizzato dal CSS del documento principale, mentre il secondo è stilizzato dal CSS dello Shadow DOM. Gli stili definiti nello Shadow DOM non influenzano gli elementi del documento principale, e gli stili definiti nel documento principale non influenzano gli elementi dello Shadow DOM. Questo dimostra come lo Shadow DOM incapsuli gli stili, assicurando che non vi siano conflitti tra gli stili del componente e gli stili globali.

Selettori Speciali per lo Shadow DOM

L'incapsulamento non significa isolamento totale. Tre selettori offrono punti di accesso controllati attraverso il confine:

  • :host — usato all'interno dell'albero shadow per stilizzare l'elemento host stesso. :host(.active) corrisponde solo quando l'host porta quella classe.
  • ::slotted(selector) — usato all'interno dell'albero shadow per stilizzare i nodi del Light DOM proiettati in uno slot. Può fare riferimento solo agli elementi di primo livello inseriti tramite slot, non ai loro discendenti.
  • ::part(name) — usato nel documento esterno per stilizzare un elemento interno che il componente espone esplicitamente con un attributo part="name". Questo è il modo ufficiale per consentire ai consumatori di personalizzare un componente senza accedere ai suoi interni.
<body>
  <div id="theme-host">
    <span>Projected from the light DOM</span>
  </div>

  <style>
    /* Outer page can only reach parts the component exposes */
    #theme-host::part(label) {
      text-decoration: underline;
    }
  </style>

  <script>
    const host = document.getElementById('theme-host');
    const root = host.attachShadow({ mode: 'open' });

    root.innerHTML = `
      <style>
        :host { display: block; padding: 10px; border: 2px solid teal; }
        .label { font-weight: bold; color: teal; }
        ::slotted(span) { color: crimson; }
      </style>
      <div class="label" part="label">Styled with :host and ::part</div>
      <slot></slot>
    `;
  </script>
</body>

La regola :host delimita l'intero componente, .label è interno e privato, ::slotted(span) colora il testo del Light DOM proiettato, e ::part(label) consente alla pagina esterna di sottolineare l'etichetta per cui ha ricevuto il permesso di applicare stili. Tutto ciò che non è esposto come part rimane inaccessibile dall'esterno.

Slot: Contenuto del Light DOM nello Shadow DOM

Gli slot consentono agli sviluppatori di passare contenuto del Light DOM (DOM normale) nello Shadow DOM, rendendo lo Shadow DOM più flessibile e riutilizzabile.

<div id="slot-host">
  <span slot="title">Shadow DOM Slot Example</span>
</div>

<script>
  const slotHost = document.getElementById('slot-host');
  const shadowRoot = slotHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <style>
      .container {
        border: 1px solid #ccc;
        padding: 10px;
      }
    </style>
    <div class="container">
      <h1><slot name="title"></slot></h1>
      <p>This is a Shadow DOM component with a slot for the title.</p>
    </div>
  `;
</script>

In questo esempio, l'elemento <slot> viene utilizzato per passare contenuto dal Light DOM allo Shadow DOM. L'attributo slot sull'elemento span corrisponde all'attributo name dell'elemento slot nello Shadow DOM, consentendo la proiezione del contenuto dello span nello Shadow DOM.

Interazione JavaScript con lo Shadow DOM

Interagire con lo Shadow DOM tramite JavaScript richiede di comprendere i confini dell'incapsulamento. La manipolazione diretta all'interno della shadow root è semplice, ma l'interazione esterna richiede una gestione attenta.

Accedere agli Elementi dello Shadow DOM

Per accedere agli elementi all'interno di uno Shadow DOM, si utilizza la proprietà shadowRoot.

<div id="interactive-host"></div>

<script>
  const interactiveHost = document.getElementById('interactive-host');
  const shadowRoot = interactiveHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <button id="shadow-btn">Click me</button>
  `;

  const shadowButton = shadowRoot.querySelector('#shadow-btn');
  shadowButton.addEventListener('click', () => {
    alert('Button inside Shadow DOM clicked!');
  });
</script>

In questo esempio, accediamo al pulsante all'interno dello Shadow DOM usando querySelector sulla shadow root. Poiché la shadow root è aperta, possiamo collegare listener di eventi e manipolare gli elementi direttamente dal documento principale.

Retargeting degli Eventi

Gli eventi che risalgono dall'albero shadow verso l'esterno vengono retargetati: per i listener nel documento esterno, event.target punta all'host shadow, non all'elemento interno che è stato effettivamente cliccato. Questo mantiene privata la struttura interna. All'interno dell'albero shadow, il target reale è ancora disponibile tramite event.composedPath()[0] o event.target.

<div id="event-host"></div>

<script>
  const host = document.getElementById('event-host');
  const root = host.attachShadow({ mode: 'open' });
  root.innerHTML = '<button id="inner">Click me</button>';

  // Listener in the OUTER document
  document.addEventListener('click', (e) => {
    console.log('Outer target:', e.target.id || e.target.tagName);
    console.log('Real target:', e.composedPath()[0].id);
  });
</script>

Cliccando il pulsante vengono registrati Outer target: event-host (retargetato all'host) e Real target: inner da composedPath(). Nota che gli eventi personalizzati attraversano il confine shadow solo se creati con { bubbles: true, composed: true }.

Esempi Pratici di Shadow DOM

Creare un Web Component Riutilizzabile

Creare un web component riutilizzabile usando lo Shadow DOM comporta la definizione di un elemento personalizzato e il collegamento di una shadow root ad esso.

<body>
  <custom-card title="Hello World"></custom-card>

  <script>
    class CustomCard extends HTMLElement {
      constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
          <style>
            .card {
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 5px;
              box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            }
            .card-title {
              font-size: 1.2em;
              margin-bottom: 5px;
            }
          </style>
          <div class="card">
            <div class="card-title">${this.getAttribute('title')}</div>
            <div class="card-content"><slot></slot></div>
          </div>
        `;
      }
    }

    customElements.define('custom-card', CustomCard);
  </script>
</body>

In questo esempio viene creato un elemento personalizzato <custom-card> con uno Shadow DOM. Lo Shadow DOM incapsula gli stili e la struttura del componente, rendendolo riutilizzabile senza preoccuparsi dei conflitti di stile con il documento principale. Abbinare lo Shadow DOM agli elementi personalizzati e all'elemento <template> è la ricetta standard per i Web Components in produzione.

Integrazione con i Framework

Lo Shadow DOM può essere usato in modo trasparente con i moderni framework JavaScript come React, Angular e Vue.

Esempio con React

In React, puoi collegare uno Shadow DOM a un elemento contenitore nel seguente modo:

<body>
  <div id="root"></div>
  <!-- React and ReactDOM CDN links -->
  <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script type="text/babel">
    const { useRef, useLayoutEffect } = React;

    const CustomCard = ({ title, content }) => {
      const cardRef = useRef(null);

      useLayoutEffect(() => {
        if (cardRef.current) {
          const shadowRoot = cardRef.current.attachShadow({ mode: 'open' });

          shadowRoot.innerHTML = `
            <style>
              .card {
                padding: 10px;
                border: 1px solid #ddd;
                border-radius: 5px;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
              }
              .card-title {
                font-size: 1.2em;
                margin-bottom: 5px;
              }
            </style>
            <div class="card">
              <div class="card-title">${title}</div>
              <div class="card-content">${content}</div>
            </div>
          `;
        }
      }, [title, content]);

      return <div ref={cardRef}></div>;
    };

    const App = () => (
      <CustomCard title="Hello World" content="This is content inside the shadow DOM.">
      </CustomCard>
    );

    const rootElement = document.getElementById('root');
    const root = ReactDOM.createRoot(rootElement);
    root.render(<App />);
  </script>
</body>

In questo esempio viene creato un componente React CustomCard che collega uno Shadow DOM a un normale div. Lo Shadow DOM garantisce che gli stili e la struttura del componente siano incapsulati, fornendo un'integrazione trasparente con React.

Quando Usare lo Shadow DOM

Lo Shadow DOM non è necessario per ogni componente, quindi è importante valutarlo tenendo conto dei compromessi:

  • Usalo quando distribuisci un widget autonomo e riutilizzabile — specialmente uno utilizzato su pagine il cui CSS globale non controlli (embed, primitive di design system, widget di terze parti).
  • Evitalo quando il tuo componente vive interamente all'interno di un'applicazione che già delimita gli stili (CSS Modules, stili con scope, BEM) e vuoi che lo stile globale fluisca liberamente al suo interno.
  • Attenzione a questi errori comuni:
    • I fogli di stile e i font globali non vengono ereditati automaticamente; dichiara ciò di cui hai bisogno all'interno della root, oppure passa valori con proprietà personalizzate CSS (--my-color), che attraversano il confine.
    • Gli elementi associati ai form necessitano di configurazione aggiuntiva (l'API ElementInternals) per partecipare a un <form> esterno.
    • Il rendering lato server degli alberi shadow richiede Declarative Shadow DOM (<template shadowrootmode="open">).
Informazione

Regola pratica: preferisci mode: 'open' ed esponi gli hook di personalizzazione con ::part() e le proprietà personalizzate CSS. Usa closed solo quando nascondere gli interni è un requisito reale.

Conclusione

Padroneggiare lo Shadow DOM è essenziale per lo sviluppo web moderno, in quanto offre potente incapsulamento e riutilizzabilità. Comprendendo e applicando i concetti e gli esempi forniti, è possibile creare componenti robusti e isolati che migliorano la manutenibilità e la scalabilità delle applicazioni web.

Questa guida completa costituisce una solida base per esplorare e utilizzare lo Shadow DOM nei tuoi progetti. Che tu stia costruendo semplici widget o applicazioni complesse, lo Shadow DOM offre l'incapsulamento e la flessibilità necessari per garantire che i tuoi componenti rimangano isolati e gestibili.

Pratica

Pratica
Quale metodo viene usato per creare una shadow root in JavaScript?
Quale metodo viene usato per creare una shadow root in JavaScript?
Pratica
Cosa restituisce host.shadowRoot quando la root è stata creata con mode: 'closed'?
Cosa restituisce host.shadowRoot quando la root è stata creata con mode: 'closed'?
Pratica
Quale selettore consente al documento esterno di stilizzare solo le parti interne che un componente espone esplicitamente?
Quale selettore consente al documento esterno di stilizzare solo le parti interne che un componente espone esplicitamente?
Was this page helpful?