LEARNING OBJECTIVES โต
- Understand what slotted (projected) nodes are and where they reside in the DOM hierarchy.
- Target projected Light DOM elements using the
::slotted(<compound-selector>)pseudo-element. - Master the Direct Child Limitation: understand why
::slotted()cannot select nested descendant children of slotted nodes. - Analyze the specificity and cascade precedence between outer Light DOM styles and internal
::slotted()styles. - Style both default and named slots across dynamic component layouts.
๐ The Mental Model & Story (Intuitive Foundation)
Imagine a high-end picture frame displayed in an art gallery.
- The Frame (
Shadow Tree): Built with custom mahogany wood, glass, and internal LED spotlights. - The Artwork (
Slotted Light DOM Content): Created and owned by an external artist (the parent document). The artist places their painting inside the frame's slot. - Styling with
::slotted()(The Frame's LED Spotlight): From inside the frame, the LED spotlights can cast a soft warm glow onto the direct surface of the canvas (::slotted(img) { border-radius: 4px; }). - The Limitation (Cannot Touch the Artist's Brushstrokes): The frame's lighting cannot reach inside the canvas to repaint individual paint strokes inside the painting (
::slotted(div) pis invalid). Furthermore, if the artist brings their own physical varnish or ink (Light DOM CSS), the artist's styling takes precedence over the frame's lighting.
LIGHT DOM (The Artist's Painting)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ <my-card> โ
โ <h3 slot="title" class="artist-title">Project X</h3>โ <--- DIRECT SLOTTED NODE
โ <div slot="body"> โ <--- DIRECT SLOTTED NODE
โ <p>Nested child paragraph</p> โ <--- NESTED DESCENDANT (Untouchable by ::slotted)
โ </div> โ
โ </my-card> โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Projected into Slot
โผ
SHADOW TREE (The Frame)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ #shadow-root โ
โ <slot name="title"></slot> โ
โ <slot name="body"></slot> โ
โ โ
โ ::slotted(h3) ===> MATCHES direct <h3 slot="title"> โ
โ ::slotted(div) ===> MATCHES direct <div slot="body"> โ
โ ::slotted(p) ===> โ FAILS! (p is inside div) โ
โ ::slotted(div p) ===> โ INVALID CSS SYNTAX! โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Technical Deep Dive & Specifications
1. What is Content Projection?
When a custom element contains children in the Light DOM, the browser does not move them in the DOM tree. Instead, it projects them through <slot> elements into the visual Flat Tree.
<!-- Light DOM (Host Document) -->
<article-card>
<h2 slot="headline">Breaking Technology News</h2>
</article-card>
<!-- Inside Component's Shadow DOM -->
<div class="card-wrapper">
<slot name="headline"></slot>
</div>
2. The ::slotted() Selector Syntax
Inside the shadow root stylesheet, you use ::slotted() to apply default styles to projected elements:
/* Styles any top-level slotted node */
::slotted(*) {
box-sizing: border-box;
}
/* Styles top-level slotted <h2> elements */
::slotted(h2) {
margin: 0 0 10px 0;
font-family: var(--font-heading, sans-serif);
color: #1e293b;
}
/* Styles top-level slotted elements having class .lead */
::slotted(.lead) {
font-size: 1.15rem;
color: #64748b;
}
3. The Direct-Child Restriction (Crucial Spec Rule)
According to the CSS Scoping Module Level 1 specification:
The
::slotted()pseudo-element matches only the first-level child nodes distributed to a slot. It never matches descendants of those nodes.
<!-- Light DOM -->
<user-card>
<div class="user-info">
<span class="user-name">Sarah Connor</span>
</div>
</user-card>
| Selector in Shadow Root | Matches? | Reason |
|---|---|---|
::slotted(.user-info) |
โ YES | .user-info is the direct slotted node |
::slotted(div) |
โ YES | Direct slotted tag matches div |
::slotted(.user-name) |
โ NO | .user-name is a descendant inside div |
::slotted(div) .user-name |
โ NO | Combinators after ::slotted() targeting descendants are invalid |
::slotted(div > span) |
โ NO | Argument inside ::slotted() must be a single compound selector |
4. Light DOM vs. Shadow DOM Specificity on Slotted Content
Slotted elements live in the Light DOM, which means they are subject to both Light DOM stylesheets and Shadow DOM ::slotted() styles.
Specificity Order for Slotted Elements (Lowest to Highest):
1. Shadow DOM default fallback content inside <slot>Fallback</slot>
2. Shadow DOM `::slotted(...)` styles
3. Outer Light DOM styles targeting the element (e.g. `article-card h2`)
4. Outer Light DOM styles with `!important`
[!IMPORTANT] Light DOM Always Wins: If an outer stylesheet has
h2 { color: blue; }and the shadow root has::slotted(h2) { color: red; }, the rendered text will be blue! Light DOM rules naturally override shadow::slotted()rules unless::slotted()uses!important.
๐ป Interactive Code Playground
Starter Code
Save this file as slotted-demo.html and open it in your browser:
Line-by-Line Code Breakdown
- Line 66โ71:
::slotted(img[slot="media"])enforces image geometry (width: 100%,height: 180px,object-fit: cover) on the slotted image tag. - Line 74โ78:
::slotted(h2[slot="title"])provides baseline title typography. - Line 14โ17: The outer document defines
.custom-title { color: #38bdf8; }. Because outer Light DOM rules take precedence over shadow::slotted()rules, the title text renders sky blue. - Line 81โ86:
::slotted(p)formats all paragraphs projected into the default unnamed slot.
๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Build a Reusable Tab Panel with Styled Slotted Tabs
Scenario: You are building an accessible <tab-group> custom element. Consumers project their tab buttons and content panels into slots. You must use ::slotted() to format the projected elements into clean, modern UI components.
Instructions:
- Create a custom element
<tab-group>with an open shadow root. - In the shadow template, provide
<slot name="tabs"></slot>inside a tab bar container, and<slot name="panels"></slot>inside a content area. - Use
::slotted(button[slot="tabs"])to style tab triggers (background: transparent,border: none,color: #94a3b8,padding: 10px 16px,cursor: pointer,font-size: 14px,border-bottom: 2px solid transparent). - Use
::slotted(button[slot="tabs"][aria-selected="true"])to highlight the active tab (color: #38bdf8,border-bottom-color: #38bdf8,font-weight: 600). - Use
::slotted(section[slot="panels"])to style the tab panels (padding: 16px,color: #e2e8f0).
๐ Starter Code Sandbox
โ ๏ธ Common Pitfalls
- Trying to Style Slotted Descendants:
::slotted(div) spanor::slotted(div span)does not work.::slotted()can only select direct children assigned to the slot. - Forgetting that Light DOM CSS beats
::slotted(): If an element inside a slot has an outer CSS rule, the outer rule will override your::slotted()rule. Use::slotted()for sensible defaults, not mandatory overrides.
๐ก Pro Tips
- Compound Selector Inside Parentheses: You can pass complex compound selectors inside
::slotted(), e.g.,::slotted(a.btn[target="_blank"]:hover). - Combine with Slot Name Attribute: Use
::slotted([slot="header"])to explicitly target content intended for specific slots.
๐ Key Takeaways
- Slotted nodes remain in the Light DOM; they are only visually projected into the shadow tree.
- The
::slotted(selector)pseudo-element styles direct children assigned to a<slot>. ::slotted()cannot target nested descendants of slotted elements.- Light DOM stylesheets take precedence over shadow
::slotted()rules. ::slotted(*)is useful for setting universal base margins and box-sizing on all projected nodes.- --