Introduction to HTML Slot Elements
Modern web development increasingly relies on reusable components and encapsulated UI patterns. Web Components provide a native way to achieve this, and one of their most powerful features is the <slot> element. Slots bridge the gap between the shadow DOM (the component's internal structure) and the light DOM (the content provided by the component's user). Understanding how to implement slots effectively transforms static components into flexible, composable building blocks.
What is a Slot?
A slot is a placeholder inside a component's shadow DOM where developers can insert their own HTML content. When you define a custom element with an attached shadow root, the markup inside that root is encapsulated — styles don't leak in or out, and selectors from the outside world cannot reach it. However, a slot acts as a controlled opening: any child nodes placed between the custom element's tags in the main document (light DOM) are projected into the slot location, replacing the slot's fallback content if present.
Think of it like a doorway. The shadow DOM defines the room layout and decoration. The light DOM provides the furniture that passes through the doorway (the slot) and arranges itself inside.
Why Slots Matter
- Reusability without rigidity: A component can define its core structure (header, body, footer) but let consumers supply the actual content, making the component adaptable across contexts.
- Encapsulation with flexibility: Styles and behavior inside the shadow DOM stay isolated, yet the component remains open for external content injection.
- Composition over inheritance: Slots encourage building UIs by combining smaller pieces (composition) rather than relying on deep inheritance hierarchies.
- Progressive enhancement: Browsers that don't support shadow DOM still render the light DOM content, ensuring a usable baseline.
Basic Implementation: Named and Default Slots
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Every slot inside a shadow DOM can be either a default slot (unnamed) or a named slot (with a name attribute). The consumer assigns content to a named slot by setting the slot attribute on their elements. Content without a slot attribute is distributed to the default slot.
Default Slot Example
Here is a simple <user-card> component that uses a default slot to display user-provided content:
<!-- Custom element definition in a script -->
<script>
customElements.define('user-card', class extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
font-family: sans-serif;
}
</style>
<div class="card">
<slot>No user information provided.</slot>
</div>
`;
}
});
</script>
<!-- Usage in HTML document -->
<user-card>
<p><strong>Name:</strong> Jane Doe</p>
<p><strong>Email:</strong> jane@example.com</p>
</user-card>
In this example, the <slot> element has no name attribute, making it the default slot. The two paragraphs inside <user-card> are projected into that slot, replacing the fallback text "No user information provided." If you use <user-card></user-card> with no children, the fallback is rendered instead.
Named Slots Example
For more granular control, assign names to slots and target them with the slot attribute:
<script>
customElements.define('layout-panel', class extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
.container {
display: flex;
flex-direction: column;
}
header {
background: #f0f0f0;
padding: 12px;
font-weight: bold;
}
.body {
padding: 16px;
}
footer {
background: #f9f9f9;
padding: 12px;
font-size: 0.9em;
color: #666;
}
</style>
<div class="container">
<header>
<slot name="header">Default Header</slot>
</header>
<div class="body">
<slot>Default body content</slot>
</div>
<footer>
<slot name="footer">Default Footer</slot>
</footer>
</div>
`;
}
});
</script>
<!-- Usage -->
<layout-panel>
<h2 slot="header">My Custom Header</h2>
<p>This goes into the default body slot.</p>
<span slot="footer">Copyright 2025</span>
</layout-panel>
Here, the <h2> with slot="header" lands in the <slot name="header"> placeholder. The plain <p> without a slot attribute is assigned to the unnamed (default) slot. The <span slot="footer"> goes to the footer slot. Elements can target only one slot, and multiple elements can be assigned to the same named slot — they will all be projected in order.
Combining Default and Named Slots
A component can contain both default and named slots simultaneously. The default slot collects all leftover content not explicitly assigned. This pattern works well for a "card" layout where you want a title slot and a free-form content area:
<script>
customElements.define('info-card', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }).innerHTML = `
<style>
.card { border: 2px solid navy; border-radius: 6px; padding: 20px; }
.title { margin: 0 0 12px; color: navy; }
</style>
<div class="card">
<div class="title"><slot name="title">Card Title</slot></div>
<slot></slot>
</div>
`;
}
});
</script>
<info-card>
<h3 slot="title">Important Notice</h3>
<p>This is the main body text with <strong>rich</strong> formatting.</p>
<ul>
<li>Item one</li>
<li>Item two</li>
</ul>
</info-card>
The <h3 slot="title"> fills the named slot, while the paragraph and unordered list automatically go to the unnamed slot.
Advanced Slot Techniques
Once you grasp the basics, you can leverage slot events, styling hooks, and the JavaScript slot API to build more interactive and intelligent components.
Slotchange Event
Whenever the set of slotted nodes changes (due to DOM manipulation or initial rendering), the slot element fires a slotchange event. You can listen for this to react dynamically.
customElements.define('event-logger', class extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `<slot>No events logged</slot>`;
const slot = shadow.querySelector('slot');
slot.addEventListener('slotchange', (e) => {
console.log('Slot content changed');
const assignedNodes = slot.assignedNodes({ flatten: false });
console.log('Assigned nodes:', assignedNodes);
// You can perform actions like updating a counter or validating content
});
}
});
Keep in mind that slotchange fires when the direct assigned nodes change. It does not fire for changes within those nodes (like text content mutation) unless the slot's child elements themselves are added or removed. For observing deeper changes, you'd need a MutationObserver on the assigned nodes.
Styling Slotted Elements
By default, slotted content inherits styles from the light DOM and also from any styles applied by the host document. But you can also target slotted elements from within the shadow DOM using the ::slotted() pseudo-element selector. This selector only works on elements directly assigned to a slot, not on their descendants.
// Inside a shadow root
<style>
::slotted(p) {
color: darkblue;
font-weight: bold;
}
::slotted(.special) {
background: yellow;
}
/* This will NOT work because span is nested inside a slotted p */
::slotted(p span) {
color: red;
}
</style>
<slot></slot>
The ::slotted selector can only accept a simple selector (element, class, attribute). Compound selectors like ::slotted(p span) are invalid. This limitation stems from the fact that the light DOM remains outside the shadow boundary, so only the top-level slotted node can be targeted.
Accessing Slotted Nodes Programmatically
Every <slot> element provides two crucial methods:
slot.assignedNodes({ flatten: true/false })— returns an array of nodes assigned to this slot. Ifflattenis true, it also resolves any nested slots (e.g., if a slotted element is itself a custom element with its own slots).slot.assignedElements()— returns only the element nodes (ignoring text nodes).
Each slotted element also has an assignedSlot property, which points to the <slot> element it is projected into (or null if not slotted).
// Inside a component's connectedCallback
connectedCallback() {
const slot = this.shadowRoot.querySelector('slot[name="actions"]');
const buttonElements = slot.assignedElements(); // array of HTML elements
buttonElements.forEach(btn => {
btn.addEventListener('click', this.handleAction.bind(this));
});
}
This pattern lets you attach event listeners to slotted buttons or links without querying the entire document. It respects encapsulation while keeping interactivity in the component's control.
Best Practices for Using Slots
- Provide meaningful fallback content: Every slot should have sensible default content so the component remains useful even when consumers omit certain pieces.
- Use named slots for distinct regions: Separate headers, footers, and sidebars with named slots; reserve the default slot for the main free-form area.
- Keep light DOM content simple: Avoid complex logic or styling that depends on the light DOM's internal structure — slotted content should be as presentation-agnostic as possible.
- Don't over-engineer with slotchange: Use the
slotchangeevent sparingly; heavy DOM manipulation in its handler can cause performance bottlenecks. Prefer declarative approaches. - Style with
::slotted()cautiously: Because of selector limitations, apply broad styling hints (like::slotted(*)or class-based) and let the light DOM handle detailed styling. - Combine slots with CSS parts: When you need more styling hooks for deeply nested shadow elements, use the
::part()CSS feature alongside slots to expose internal parts for external styling. - Test with empty and varied content: Ensure your component gracefully handles no slotted content, only partial slots, or dynamically added/removed nodes.
- Document slot names and expected content: Treat slot names as part of your component's public API. Clear documentation helps consumers use the component correctly.
Conclusion
HTML Slot elements are a cornerstone of modern Web Component composition. They empower developers to craft encapsulated, reusable components that still embrace external content, striking a perfect balance between isolation and flexibility. By mastering default and named slots, leveraging the slotchange event, using ::slotted() for targeted styling, and accessing assigned nodes programmatically, you can build components that adapt gracefully to a wide range of use cases. Embrace slots as your primary tool for composition, follow best practices, and your component architecture will remain clean, maintainable, and ready for the future of the web platform.