About shadow-dom-overlay-insertion
shadow-dom-overlay-insertion is a technical reference for inserting overlays from web components that may be slotted inside a shadow DOM. It describes how to choose the insertion container and handle the timing gap before Lit has rendered its shadow DOM.
Key features
- 01Detect slotted components with `assignedSlot`
- 02Insert overlays in the shadow-internal container
- 03Handle pre-render slot assignment timing
- 04Choose insertion behavior for each DOM state
Use cases
- 01Add overlays to slotted web components
- 02Prevent overlays from landing in the host light DOM
- 03Retry insertion after Lit renders
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ editframe/ skills --skill shadow-dom-overlay-insertionAfter installing, type/shadow-dom-overlay-insertionin Claude Code to confirm it loaded.
Details
From the project READMEShadow DOM Overlay Insertion
The Problem
When component A (e.g. EFCanvas) tries to insert an overlay into the DOM next to itself, it typically uses this.parentElement.appendChild(overlay). This breaks when A is slotted inside a shadow DOM host (e.g. EFWorkbench).
EFWorkbench (shadow host)
shadow root
<div class="canvas-slot-container"> ← the real DOM parent
<slot name="canvas"> ← slot element
[EFCanvas is assigned here] ← light DOM, assigned via slot
efCanvas.parentElementreturns the shadow host (EFWorkbench), not the internal container- Calling
shadowHost.appendChild(overlay)places the overlay in the host's light DOM, where CSS and z-index from the shadow DOM won't apply — the overlay is invisible
The Fix Pattern
private insertOverlay(overlay: HTMLElement): void {
const slot = this.assignedSlot;
if (slot) {
// Correctly slotted: insert into the shadow-internal container
slot.parentElement?.appendChild(overlay);
return;
}
if (this.parentElement?.shadowRoot) {
// Shadow DOM exists but slot assignment hasn't happened yet
// (connectedCallback fires before Lit renders shadow DOM).
// Return early; caller should retry via requestAnimationFrame.
return;
}
// Not slotted — plain DOM, safe to use parentElement
this.parentElement?.appendChild(overlay);
}
The Timing Hazard
During HTML parsing, connectedCallback fires before Lit has rendered the shadow DOM. At that point:
this.assignedSlotisnull(slot assignment hasn't happened)this.parentElementis non-null (the shadow host)this.parentElement.shadowRootis also null (shadow DOM not yet attached)
Later in the same microtask tick (after Lit's performUpdate):
this.assignedSlotbecomes the<slot>elementslot.parentElementis the internal container
Guard pattern:
connectedCallback() {
super.connectedCallback();
this.tryInsertOverlay();
}
private tryInsertOverlay(): void {
if (!this.assignedSlot && this.parentElement?.shadowRoot) {
// Too early — shadow host exists but slot not yet assigned.
// Lit will assign the slot after performUpdate.
requestAnimationFrame(() => this.tryInsertOverlay());
return;
}
this.insertOverlay(this.overlay);
}
Summary of Rules
| Situation | assignedSlot | parentElement.shadowRoot | Action |
|---|---|---|---|
| Not slotted | null | null/undefined | Use parentElement.appendChild |
| Correctly slotted | non-null | (any) | Use assignedSlot.parentElement.appendChild |
| Too early (Lit not rendered) | null | non-null | Return early, retry via RAF |
| Slotted, parent has no shadow | null | null | Use parentElement.appendChild |
Never use this.parentElement directly when the parent is a shadow DOM host.