Skip to content

Shadow DOM fundamentals ​

Inleiding ​

Shadow DOM is een van de kernpijlers van de Web Components standaard en biedt een krachtig mechanisme voor encapsulatie in het web platform. Het stelt ontwikkelaars in staat om geïsoleerde DOM-bomen te creëren die afgeschermd zijn van de rest van het document, waardoor modulaire, herbruikbare componenten mogelijk worden zonder zorgen over styling conflicten of onbedoelde interacties. In deze gids verkennen we de fundamenten van Shadow DOM, hoe het werkt in LIT components, en hoe je het effectief kunt gebruiken in je webapplicaties.

Wat is Shadow DOM? ​

Shadow DOM is een webtechnologie die het mogelijk maakt om een afgeschermde, geïsoleerde DOM-tree te creëren binnen een element. Deze geïsoleerde DOM-tree wordt een "shadow tree" genoemd, en het element waaraan deze is gekoppeld wordt de "shadow host" genoemd.

De belangrijkste kenmerken van Shadow DOM zijn:

  1. DOM Encapsulatie: Shadow DOM creëert een afgeschermde DOM-tree die niet direct toegankelijk is vanuit het hoofddocument.

  2. Styling Encapsulatie: CSS-stijlen binnen een shadow tree blijven binnen die boom en beïnvloeden de rest van de pagina niet.

  3. Compositie: Shadow DOM maakt het mogelijk om content van buiten (Light DOM) te projecteren in specifieke plaatsen binnen de shadow tree via slots.

  4. Scoping: JavaScript binnen een shadow tree heeft een beperkte scope, wat helpt bij het voorkomen van naamconflicten en onbedoelde interacties.

Shadow DOM vs. Light DOM ​

Om Shadow DOM goed te begrijpen, is het belangrijk om het verschil te kennen met wat we "Light DOM" noemen:

Light DOM is de reguliere DOM die we allemaal kennen - de elementen die direct in het HTML-document worden geschreven en volledig toegankelijk zijn via standaard DOM-methoden zoals querySelector.

html
<!-- Light DOM -->
<date-picker>
  <p>Dit is Light DOM content</p>
</date-picker>

Shadow DOM is een geïsoleerde DOM-tree die aan een element is gekoppeld en afgeschermd is van de rest van het document.

html
<!-- Het element met Shadow DOM -->
<date-picker>
  <!-- Light DOM (zichtbaar in de pagina DOM) -->
  <p>Dit is Light DOM content</p>

  <!-- Shadow DOM (niet direct zichtbaar in de pagina DOM) -->
  #shadow-root
  <div class="container">
    <slot></slot>
    <!-- Hier wordt de Light DOM content geprojecteerd -->
  </div>
</date-picker>

Het belangrijkste verschil is dat Light DOM direct deel uitmaakt van het hoofddocument, terwijl Shadow DOM een afgeschermde, geïsoleerde DOM-tree is die aan een element is gekoppeld.

Voordelen van Shadow DOM ​

  1. CSS-encapsulatie - Voorkomt styling conflicten doordat CSS-regels binnen de component blijven en externe stijlen niet binnendringen.

  2. DOM-encapsulatie - Beschermt interne DOM-structuur tegen onbedoelde externe manipulatie en selector-conflicten.

  3. Compositie via slots - Biedt een standaard, declaratief mechanisme voor content projectie in specifieke plaatsen binnen componenten.

  4. Duidelijke component boundaries - Creëert een heldere scheiding tussen component en applicatie, wat modulariteit bevordert.

  5. Event retargeting - Zorgt voor veilige event propagatie over component grenzen heen, met behoud van implementatiedetails.

  6. Prestatievoordelen - Maakt browser-optimalisaties mogelijk door geïsoleerde DOM-subtrees en beperkte style recalculation.

  7. Standaardisatie - Is een webstandaard die werkt in alle moderne browsers, onafhankelijk van frameworks.

  8. Verbeterde debugging - Maakt problemen gemakkelijker te lokaliseren door duidelijk zichtbare component grenzen in DevTools.

  9. Progressieve verbetering - Ondersteunt fallback content en polyfills voor oudere browsers.

  10. Design system integratie - Werkt naadloos samen met design systems via CSS Custom Properties en gecontroleerde styling.

Shadow DOM aanmaken ​

Er zijn twee manieren om Shadow DOM aan te maken:

1. Imperatief met vanilla JavaScript ​

javascript
// Een element selecteren om als shadow host te dienen
const host = document.getElementById('host');

// Shadow DOM aanmaken en koppelen aan de host
// 'open' betekent dat de shadow root toegankelijk is via element.shadowRoot
const shadowRoot = host.attachShadow({ mode: 'open' });

// Content toevoegen aan de shadow root
const div = document.createElement('div');
div.textContent = 'Dit is content in de shadow DOM';
shadowRoot.appendChild(div);

const slot = document.createElement('slot');
shadowRoot.appendChild(slot);

2. Declaratief met LIT ​

In LIT components wordt Shadow DOM automatisch aangemaakt wanneer je een component definieert:

typescript
// date-picker.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './date-picker.scss';

@customElement('date-picker')
class DatePicker extends LitElement {
  static styles = styles;

  render() {
    return html`
      <p>Dit is content in de shadow DOM</p>
      <slot></slot>
    `;
  }
}
scss
// date-picker.scss
@use '@diekeure/cds-tokens' as cds;

p {
  color: rgb(cds.get('cds.sys.color.on-surface'));
}

Shadow DOM structuur ​

Een Shadow DOM-structuur bestaat uit verschillende sleutelelementen:

  1. Shadow Host: Het reguliere DOM-element waaraan de Shadow DOM is gekoppeld.
  2. Shadow Root: Het root-element van de Shadow DOM-tree.
  3. Shadow Tree: De DOM-subtree binnen de Shadow Root.
  4. Shadow Boundary: De conceptuele grens tussen de Shadow DOM en de Light DOM.
<date-picker>                 <!-- Shadow Host -->
  <p>Light DOM content</p>    <!-- Light DOM -->

  #shadow-root                <!-- Shadow Root -->
    <div>...</div>            <!-- Shadow Tree -->
    <slot></slot>             <!-- Insertion point voor Light DOM -->
</date-picker>

Encapsulatie en styling ​

Een van de belangrijkste voordelen van Shadow DOM is encapsulatie. Dit betekent dat:

  1. DOM Encapsulatie: JavaScript van buiten kan niet direct elementen binnen de Shadow DOM selecteren zonder eerst de Shadow Root te benaderen.
javascript
// Dit werkt NIET om elementen binnen shadow DOM te selecteren
document.querySelector('date-picker p');

// Dit werkt WEL
document.querySelector('date-picker').shadowRoot.querySelector('p');
  1. CSS Encapsulatie: Stijlen binnen Shadow DOM blijven binnen de Shadow Tree en beïnvloeden de rest van de pagina niet. Omgekeerd beïnvloeden stijlen van buiten de Shadow DOM niet de elementen binnen de Shadow DOM (met enkele uitzonderingen).
scss
// global.scss - Deze stijl beïnvloedt NIET de paragrafen binnen shadow DOM
p {
  color: blue;
}

// date-picker.scss - Deze stijl beïnvloedt ALLEEN paragrafen binnen deze shadow tree
p {
  color: red;
}

Slots en content projectie ​

Slots zijn een cruciaal onderdeel van Shadow DOM dat het mogelijk maakt om Light DOM-content te projecteren in specifieke plaatsen binnen de Shadow DOM. Dit wordt "content projectie" genoemd.

Basis slot ​

Een basis slot projecteert alle Light DOM-content:

typescript
// product-card.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './product-card.scss';

@customElement('product-card')
class ProductCard extends LitElement {
  static styles = styles;

  render() {
    return html`
      <div class="card">
        <slot></slot>
        <!-- Alle Light DOM-content komt hier -->
      </div>
    `;
  }
}
scss
// product-card.scss
@use '@diekeure/cds-tokens' as cds;

.card {
  border: 1px solid rgb(cds.get('cds.sys.color.outline'));
  padding: cds.get('cds.sys.spacing.s');
  border-radius: cds.get('cds.sys.shape.medium');
}

Gebruik:

html
<product-card>
  <h2>Smartphone XS</h2>
  <p>De nieuwste smartphone met verbeterde camera</p>
</product-card>

Named slots ​

Named slots maken het mogelijk om specifieke Light DOM-content op specifieke plaatsen te projecteren:

typescript
// product-details.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './product-details.scss';

@customElement('product-details')
class ProductDetails extends LitElement {
  static styles = styles;

  render() {
    return html`
      <div class="product">
        <div class="header">
          <slot name="title">Productnaam</slot>
        </div>
        <div class="content">
          <slot>Productbeschrijving</slot>
        </div>
        <div class="footer">
          <slot name="actions">Geen acties beschikbaar</slot>
        </div>
      </div>
    `;
  }
}
scss
// product-details.scss
@use '@diekeure/cds-tokens' as cds;

.product {
  border: 1px solid rgb(cds.get('cds.sys.color.outline'));
  border-radius: cds.get('cds.sys.shape.medium'));
}

.header {
  padding: cds.get('cds.sys.spacing.xs') cds.get('cds.sys.spacing.s');
  border-bottom: 1px solid rgb(cds.get('cds.sys.color.outline-variant'));
  font-weight: bold;
}

.content {
  padding: cds.get('cds.sys.spacing.s');
}

.footer {
  padding: cds.get('cds.sys.spacing.xs') cds.get('cds.sys.spacing.s');
  border-top: 1px solid rgb(cds.get('cds.sys.color.outline-variant'));
}

Gebruik:

html
<product-details>
  <h2 slot="title">Smartphone XS</h2>
  <p>De nieuwste smartphone met verbeterde camera</p>
  <button slot="actions">Toevoegen aan winkelwagen</button>
</product-details>

In dit voorbeeld:

  • De <h2> wordt geprojecteerd in de slot met name="title"
  • De <p> wordt geprojecteerd in de default slot (zonder naam)
  • De <button> wordt geprojecteerd in de slot met name="actions"

Styling van Shadow DOM ​

Stijlen binnen Shadow DOM ​

Stijlen binnen Shadow DOM blijven geïsoleerd. In ons project gebruiken we SCSS-bestanden met CDS design tokens:

typescript
// product-tile.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './product-tile.scss';

@customElement('product-tile')
class ProductTile extends LitElement {
  static styles = styles;

  render() {
    return html`
      <div class="container">
        <p>Deze tekst gebruikt CDS kleuren en spacing</p>
        <slot></slot>
      </div>
    `;
  }
}
scss
// product-tile.scss
@use '@diekeure/cds-tokens' as cds;

p {
  color: rgb(cds.get('cds.sys.color.on-surface'));
  margin-bottom: cds.get('cds.sys.spacing.xs');
}

.container {
  padding: cds.get('cds.sys.spacing.s');
  background-color: rgb(cds.get('cds.sys.color.surface'));
  border-radius: cds.get('cds.sys.shape.medium');
}

Styling van geprojecteerde content ​

Er zijn verschillende manieren om geprojecteerde content te stylen:

1. ::slotted() pseudo-element ​

Het ::slotted() pseudo-element stelt je in staat om geprojecteerde elementen te stylen vanuit de Shadow DOM:

scss
// product-card.scss
@use '@diekeure/cds-tokens' as cds;

.card {
  border: 1px solid rgb(cds.get('cds.sys.color.outline'));
  padding: cds.get('cds.sys.spacing.s');
}

/* Stijl voor alle geprojecteerde paragrafen */
::slotted(p) {
  color: rgb(cds.get('cds.sys.color.primary'));
  margin: cds.get('cds.sys.spacing.2xs') 0;
}

/* Stijl voor alle geprojecteerde elementen */
::slotted(*) {
  font-family: var(--dcr-comp-product-card-font, sans-serif);
}

Beperkingen van ::slotted():

  • Het kan alleen directe slotted elementen selecteren, niet hun kinderen
  • Het kan niet worden gecombineerd met andere selectors (zoals ::slotted(p) span)

2. CSS-variabelen (Custom Properties) ​

CSS-variabelen doorbreken de Shadow Boundary en zijn een krachtige manier om styling aan te passen. In ons project gebruiken we de naamgevingsconventie --dcr-comp-<comp-name>-<property> in combinatie met CDS tokens:

typescript
// notification-badge.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './notification-badge.scss';

@customElement('notification-badge')
class NotificationBadge extends LitElement {
  static styles = styles;

  render() {
    return html`<slot>0</slot>`;
  }
}
scss
// notification-badge.scss
@use '@diekeure/cds-tokens' as cds;

:host {
  /* Standaardwaarden definiëren volgens onze naamgevingsconventie */
  --dcr-comp-notification-badge-bg: cds.get('cds.sys.color.error');
  --dcr-comp-notification-badge-color: cds.get('cds.sys.color.on-error');
  --dcr-comp-notification-badge-size: cds.get('cds.sys.spacing.m');

  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

:host(:not([tiny])) {
  @include cds.font('label.large');

  min-width: 16px;
  height: 16px;

  padding-inline: cds.get('cds.sys.spacing.3xs');
  background-color: rgb(var(--dcr-comp-notification-badge-bg));
  color: rgb(var(--dcr-comp-notification-badge-color));
  border-radius: cds.get('cds.sys.shape.full');
}

Gebruik vanuit de Light DOM:

scss
// app.scss
@use '@diekeure/cds-tokens' as cds;

.warning-notifications notification-badge {
  --dcr-comp-notification-badge-bg: cds.get('cds.sys.color.warning');
  --dcr-comp-notification-badge-color: cds.get('cds.sys.color.on-warning');
}
html
<notification-badge>5</notification-badge>
<div class="warning-notifications">
  <notification-badge>3</notification-badge>
</div>

:host en :host() selectors ​

De :host selector stelt je in staat om de Shadow Host zelf te stylen vanuit de Shadow DOM:

scss
// card-component.scss
@use '@diekeure/cds-tokens' as cds;

/* Stijl voor de host element */
:host {
  display: block;
  margin: cds.get('cds.sys.spacing.xs');
  border: 1px solid rgb(cds.get('cds.sys.color.outline'));
}

/* Conditionele styling op basis van attributen */
:host([disabled]) {
  opacity: 0.5;
  pointer-events: none;
}

/* Conditionele styling op basis van klasse */
:host(.highlighted) {
  background-color: rgb(cds.get('cds.sys.color.secondary-container'));
}

Interactie tussen Shadow DOM en Light DOM ​

Event propagatie ​

Events die binnen Shadow DOM ontstaan, bubbelen omhoog en worden geretarget wanneer ze de Shadow Boundary oversteken:

<action-button>  <!-- Shadow Host -->
  #shadow-root
    <button>Verzenden</button>  <!-- Event target binnen Shadow DOM -->
</action-button>

Als de button wordt geklikt:

  1. Het event begint bij de <button> binnen Shadow DOM
  2. Wanneer het event de Shadow Boundary oversteekt, wordt event.target gewijzigd naar <action-button>
  3. Het originele pad is nog beschikbaar via event.composedPath()
javascript
document.querySelector('action-button').addEventListener('click', (event) => {
  console.log('Event target:', event.target); // <action-button>
  console.log('Original target:', event.composedPath()[0]); // <button> binnen Shadow DOM
});

Toegang tot Shadow DOM ​

1. Open vs. Closed mode ​

Bij het aanmaken van Shadow DOM, kun je kiezen tussen twee modes:

javascript
// Open mode - Shadow Root is toegankelijk via element.shadowRoot
element.attachShadow({ mode: 'open' });

// Closed mode - Shadow Root is niet direct toegankelijk
element.attachShadow({ mode: 'closed' });

In LIT wordt standaard de 'open' mode gebruikt.

2. Toegang tot elementen binnen Shadow DOM ​

typescript
// search-field.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { query } from 'lit/decorators/query.js';
import styles from './search-field.scss';

@customElement('search-field')
class SearchField extends LitElement {
  static styles = styles;

  @query('.search-input')
  searchInput!: HTMLInputElement;

  render() {
    return html`
      <input class="search-input" type="text" />
      <button @click=${this.focus}>Focus zoeken</button>
    `;
  }

  focus() {
    // Direct toegang tot een element binnen Shadow DOM
    this.searchInput.focus();
  }
}

Shadow DOM in LIT Components ​

LIT maakt het werken met Shadow DOM eenvoudig door veel van de complexiteit te abstraheren:

1. Automatische Shadow DOM creatie ​

LIT components maken automatisch een Shadow Root aan:

typescript
// user-greeting.ts
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import styles from './user-greeting.scss';

@customElement('user-greeting')
class UserGreeting extends LitElement {
  static styles = styles;

  @property()
  username = 'Gebruiker';

  render() {
    return html`<p>Welkom, ${this.username}!</p>`;
  }
}

2. Shadow DOM opties aanpassen ​

Je kunt de Shadow DOM-opties aanpassen via de shadowRootOptions static property:

typescript
// focus-delegating-form.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './focus-delegating-form.scss';

@customElement('focus-delegating-form')
class FocusDelegatingForm extends LitElement {
  static styles = styles;

  // Shadow DOM opties aanpassen
  static shadowRootOptions = {
    ...LitElement.shadowRootOptions,
    delegatesFocus: true,
  };

  render() {
    return html`
      <form>
        <input type="text" placeholder="Naam" />
        <input type="email" placeholder="Email" />
        <button type="submit">Verzenden</button>
      </form>
    `;
  }
}

3. Shadow DOM uitschakelen ​

In sommige gevallen wil je misschien geen Shadow DOM gebruiken:

typescript
// native-select-wrapper.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('native-select-wrapper')
class NativeSelectWrapper extends LitElement {
  // Shadow DOM uitschakelen
  createRenderRoot() {
    return this; // Renderen direct in de component, zonder Shadow DOM
  }

  render() {
    return html`
      <select>
        <option value="1">Optie 1</option>
        <option value="2">Optie 2</option>
      </select>
    `;
  }
}

Best practices ​

1. Gebruik slots voor flexibele componenten ​

typescript
// content-card.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './content-card.scss';

@customElement('content-card')
class ContentCard extends LitElement {
  static styles = styles;

  render() {
    return html`
      <div class="card">
        <div class="header">
          <slot name="header">Standaard header</slot>
        </div>
        <div class="content">
          <slot>Voeg content toe</slot>
        </div>
        <div class="footer">
          <slot name="footer"></slot>
        </div>
      </div>
    `;
  }
}
scss
// content-card.scss
@use '@diekeure/cds-tokens' as cds;

.card {
  border: 1px solid rgb(cds.get('cds.sys.color.outline'));
  border-radius: cds.get('cds.sys.shape.medium'));
}

.header {
  border-bottom: 1px solid rgb(cds.get('cds.sys.color.outline-variant'));
  padding: cds.get('cds.sys.spacing.xs') cds.get('cds.sys.spacing.s');
  font-weight: bold;
}

.content {
  padding: cds.get('cds.sys.spacing.s');
}

.footer {
  border-top: 1px solid rgb(cds.get('cds.sys.color.outline-variant'));
  padding: cds.get('cds.sys.spacing.xs') cds.get('cds.sys.spacing.s');
}

2. Gebruik CSS Custom Properties volgens onze naamgevingsconventie met CDS tokens ​

typescript
// action-button.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './action-button.scss';

@customElement('action-button')
class ActionButton extends LitElement {
  static styles = styles;

  render() {
    return html`<button><slot></slot></button>`;
  }
}
scss
// action-button.scss
@use '@diekeure/cds-tokens' as cds;

:host {
  /* Standaardwaarden definiëren volgens onze naamgevingsconventie */
  --dcr-comp-action-button-bg: cds.get('cds.sys.color.primary');
  --dcr-comp-action-button-color: cds.get('cds.sys.color.on-primary');
  --dcr-comp-action-button-radius: cds.get('cds.sys.shape.small');
  --dcr-comp-action-button-padding: cds.get('cds.sys.spacing.xs') cds.get('cds.sys.spacing.s');
}

button {
  @include cds.font('label.large');

  background-color: rgb(var(--dcr-comp-action-button-bg));
  color: rgb(var(--dcr-comp-action-button-color));
  border: none;
  border-radius: var(--dcr-comp-action-button-radius);
  padding: var(--dcr-comp-action-button-padding);
  cursor: pointer;
  font-family: inherit;

  &:hover {
    opacity: 0.9;
  }
}

Gebruik:

scss
// app.scss
@use '@diekeure/cds-tokens' as cds;

.danger-zone action-button {
  --dcr-comp-action-button-bg: cds.get('cds.sys.color.error');
  --dcr-comp-action-button-color: cds.get('cds.sys.color.on-error');
}

.secondary-actions action-button {
  --dcr-comp-action-button-bg: cds.get('cds.sys.color.secondary');
  --dcr-comp-action-button-radius: cds.get('cds.sys.shape.large');
}
html
<action-button>Opslaan</action-button>
<div class="danger-zone">
  <action-button>Verwijderen</action-button>
</div>
<div class="secondary-actions">
  <action-button>Annuleren</action-button>
</div>

3. Gebruik part en exportparts voor specifieke styling ​

De ::part() CSS pseudo-element biedt een gecontroleerde manier om specifieke elementen binnen Shadow DOM te stylen:

typescript
// data-table.ts
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './data-table.scss';

@customElement('data-table')
class DataTable extends LitElement {
  static styles = styles;

  render() {
    return html`
      <div>
        <div part="header">
          <slot name="header"></slot>
        </div>
        <div part="body">
          <slot></slot>
        </div>
        <div part="footer">
          <button part="pagination-button">Vorige</button>
          <span part="page-indicator">Pagina 1 van 5</span>
          <button part="pagination-button">Volgende</button>
        </div>
      </div>
    `;
  }
}
scss
// data-table.scss
@use '@diekeure/cds-tokens' as cds;

// Interne stijlen met CDS tokens

Styling vanuit Light DOM:

scss
// app.scss
@use '@diekeure/cds-tokens' as cds;

data-table::part(header) {
  background-color: rgb(cds.get('cds.sys.color.surface-variant'));
  font-weight: bold;
}

data-table::part(pagination-button) {
  background-color: transparent;
  border: 1px solid rgb(cds.get('cds.sys.color.outline'));
  padding: cds.get('cds.sys.spacing.2xs') cds.get('cds.sys.spacing.xs');
}

data-table::part(page-indicator) {
  margin: 0 cds.get('cds.sys.spacing.xs');
  font-size: 0.9em;
}

Veelvoorkomende valkuilen ​

1. Overschrijven van ingebouwde elementen zonder Shadow DOM ​

Probleem: Sommige ingebouwde elementen hebben complexe interne structuren die niet goed werken met Shadow DOM.

typescript
// enhanced-select.ts - Problematisch
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './enhanced-select.scss';

@customElement('enhanced-select')
class EnhancedSelect extends LitElement {
  static styles = styles;

  render() {
    return html`
      <select>
        <slot></slot>
      </select>
    `;
  }
}

Oplossing: Schakel Shadow DOM uit of gebruik een volledig aangepaste implementatie.

typescript
// enhanced-select.ts - Oplossing
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';

@customElement('enhanced-select')
class EnhancedSelect extends LitElement {
  // Shadow DOM uitschakelen
  createRenderRoot() {
    return this; // Geen Shadow DOM
  }

  render() {
    return html`
      <select>
        <slot></slot>
      </select>
    `;
  }
}

2. Vertrouwen op externe stijlen ​

Probleem: Externe stijlen dringen niet door in Shadow DOM.

scss
// app.scss - Dit werkt NIET voor elementen binnen Shadow DOM
p {
  color: blue;
}
html
<user-profile>
  <p>Deze tekst wordt NIET blauw</p>
</user-profile>

Oplossing: Gebruik CSS Custom Properties volgens onze naamgevingsconventie met CDS tokens.

scss
// app.scss - Oplossing
@use '@diekeure/cds-tokens' as cds;

user-profile {
  --dcr-comp-user-profile-text-color: cds.get('cds.sys.color.primary');
}

user-profile::part(content) {
  font-weight: bold;
}
html
<user-profile>
  <p>Deze tekst kan de primaire kleur krijgen als de component --dcr-comp-user-profile-text-color gebruikt</p>
</user-profile>

3. Verkeerd gebruik van slots ​

Probleem: Verkeerd begrip van hoe slots werken.

typescript
// file-upload.ts - Problematisch
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './file-upload.scss';

@customElement('file-upload')
class FileUpload extends LitElement {
  static styles = styles;

  render() {
    return html`
      <div>
        <slot id="file-slot"></slot>
      </div>
    `;
  }

  firstUpdated() {
    // Dit werkt NIET - slot.textContent geeft alleen de fallback content
    const slot = this.shadowRoot.querySelector('#file-slot');
    console.log('Slot content:', slot.textContent); // Leeg of alleen fallback
  }
}

Oplossing: Gebruik slot events en de assignedNodes API.

typescript
// file-upload.ts - Oplossing
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import styles from './file-upload.scss';

@customElement('file-upload')
class FileUpload extends LitElement {
  static styles = styles;

  render() {
    return html`
      <div>
        <slot id="file-slot" @slotchange=${this.handleSlotChange}></slot>
      </div>
    `;
  }

  handleSlotChange(e) {
    const slot = e.target;
    const nodes = slot.assignedNodes();
    console.log('Geprojecteerde nodes:', nodes);
  }
}

Conclusie ​

Shadow DOM is een krachtige technologie die de basis vormt voor moderne webcomponenten door encapsulatie van DOM-structuur en styling te bieden. Het maakt het mogelijk om echt modulaire, herbruikbare componenten te bouwen zonder zorgen over conflicten met andere delen van de applicatie.

In LIT wordt Shadow DOM naadloos geïntegreerd, waardoor het eenvoudig is om componenten te bouwen die profiteren van deze encapsulatie. Door slots, CSS Custom Properties (met onze --dcr-comp-<comp-name>-<property> naamgevingsconventie) en CDS design tokens effectief te gebruiken, kun je flexibele, themeable componenten bouwen die goed samenwerken met de rest van je applicatie.

De belangrijkste punten om te onthouden:

  1. Encapsulatie: Shadow DOM biedt zowel DOM- als CSS-encapsulatie.
  2. Slots: Gebruik slots voor flexibele content projectie.
  3. Styling: Gebruik CSS Custom Properties volgens onze naamgevingsconventie, CDS tokens, ::part() en :host voor effectieve styling.
  4. Events: Events bubbelen door Shadow DOM grenzen heen, maar worden geretarget.