Web Components
HTML5
The <slot> element is a placeholder inside a Web Component that users can fill with their own markup. It enables content projection in Shadow DOM, allowing component consumers to pass custom content into predefined areas of your component while maintaining encapsulation.
Interactive code playground requires JavaScript. Here's the code:
<template id="card-template">
<style>
.card {
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 1.5em;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
font-size: 1.3em;
font-weight: bold;
margin-bottom: 0.5em;
color: #1f2937;
}
.content {
color: #374151;
}
</style>
<div class="card">
<div class="header">
<slot name="title">Default Title</slot>
</div>
<div class="content">
<slot>Default content goes here</slot>
</div>
</div>
</template>
<script>
class SimpleCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('card-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('simple-card', SimpleCard);
</script>
<!-- Use the component -->
<simple-card>
<span slot="title">My Custom Title</span>
<p>This is my custom content that goes in the default slot!</p>
</simple-card>
<!-- Inside Shadow DOM -->
< slot ></ slot > <!-- Default slot -->
< slot name = " header " ></ slot > <!-- Named slot -->
< slot > Fallback content </ slot > <!-- Slot with fallback -->
<!-- Using slots (Light DOM) -->
< div slot = " header " > Goes to named slot </ div >
< p > Goes to default slot </ p >
Attribute Value Description nameString Identifies a named slot
The <slot> element supports all global attributes .
Understanding the difference between slot types:
Interactive code playground requires JavaScript. Here's the code:
<template id="profile-template">
<style>
.profile {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1em;
max-width: 300px;
}
.avatar { margin-bottom: 1em; }
.name { font-weight: bold; color: #1f2937; }
.bio { color: #6b7280; margin-top: 0.5em; }
</style>
<div class="profile">
<div class="avatar">
<slot name="avatar">👤</slot>
</div>
<div class="name">
<slot name="name">Anonymous</slot>
</div>
<div class="bio">
<slot name="bio">No bio provided</slot>
</div>
</div>
</template>
<script>
class UserProfile extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('profile-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('user-profile', UserProfile);
</script>
<user-profile>
<img slot="avatar" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Ccircle cx='30' cy='30' r='30' fill='%233b82f6'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='white' font-size='30'%3EJ%3C/text%3E%3C/svg%3E" alt="Avatar" width="60">
<span slot="name">Jane Developer</span>
<span slot="bio">Full-stack developer passionate about Web Components</span>
</user-profile> Interactive code playground requires JavaScript. Here's the code:
<template id="panel-template">
<style>
.panel {
border: 2px solid #3b82f6;
border-radius: 8px;
padding: 1.5em;
background: #eff6ff;
}
</style>
<div class="panel">
<slot>Default panel content</slot>
</div>
</template>
<script>
class InfoPanel extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('panel-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('info-panel', InfoPanel);
</script>
<!-- Content without slot attribute goes to default slot -->
<info-panel>
<h3>Custom Heading</h3>
<p>Any content here goes into the default slot.</p>
<button>Click Me</button>
</info-panel> Interactive code playground requires JavaScript. Here's the code:
<template id="article-template">
<style>
article {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
max-width: 500px;
}
header {
background: #3b82f6;
color: white;
padding: 1em;
}
.body {
padding: 1em;
}
footer {
background: #f3f4f6;
padding: 0.75em 1em;
border-top: 1px solid #e5e7eb;
}
</style>
<article>
<header>
<slot name="header">Article Header</slot>
</header>
<div class="body">
<slot>Article content...</slot>
</div>
<footer>
<slot name="footer">Article Footer</slot>
</footer>
</article>
</template>
<script>
class ArticleCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('article-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('article-card', ArticleCard);
</script>
<article-card>
<h2 slot="header">Understanding Slots</h2>
<p>This content goes in the default slot.</p>
<p>Multiple elements can share the same slot!</p>
<small slot="footer">Published: 2025-01-20</small>
</article-card>
Slots can have default content that appears when no content is provided:
Interactive code playground requires JavaScript. Here's the code:
<template id="fallback-template">
<style>
.container {
border: 2px dashed #3b82f6;
border-radius: 8px;
padding: 1.5em;
max-width: 400px;
}
.title {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 0.5em;
}
.default-msg {
color: #6b7280;
font-style: italic;
}
</style>
<div class="container">
<div class="title">
<slot name="title">
<span class="default-msg">No title provided</span>
</slot>
</div>
<slot>
<p class="default-msg">
No content provided. Add content to see it here!
</p>
</slot>
</div>
</template>
<script>
class FallbackDemo extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('fallback-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('fallback-demo', FallbackDemo);
</script>
<!-- With content -->
<fallback-demo>
<strong slot="title">Custom Title</strong>
<p>Custom content provided!</p>
</fallback-demo>
<!-- Without content (shows fallback) -->
<fallback-demo></fallback-demo>
Use the ::slotted() pseudo-element to style content from the light DOM:
Interactive code playground requires JavaScript. Here's the code:
<template id="styled-slot-template">
<style>
.wrapper {
padding: 1.5em;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
color: white;
}
/* Style slotted paragraphs */
::slotted(p) {
background: rgba(255, 255, 255, 0.2);
padding: 0.75em;
border-radius: 4px;
margin: 0.5em 0;
}
/* Style slotted headings */
::slotted(h3) {
margin: 0 0 0.5em 0;
font-size: 1.5em;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
/* Style slotted links */
::slotted(a) {
color: #fbbf24;
text-decoration: underline;
font-weight: bold;
}
</style>
<div class="wrapper">
<slot></slot>
</div>
</template>
<script>
class StyledSlot extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('styled-slot-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('styled-slot', StyledSlot);
</script>
<styled-slot>
<h3>Styled Content</h3>
<p>This paragraph is styled via ::slotted(p)</p>
<p>Multiple paragraphs get the same styling</p>
<a href="#">This link is styled too!</a>
</styled-slot>
Monitor when slot content changes:
Interactive code playground requires JavaScript. Here's the code:
<template id="event-template">
<style>
.monitor {
padding: 1em;
border: 2px solid #3b82f6;
border-radius: 8px;
}
.log {
margin-top: 1em;
padding: 0.75em;
background: #f3f4f6;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
</style>
<div class="monitor">
<slot id="monitored"></slot>
<div class="log" id="log">Waiting for slot changes...</div>
</div>
</template>
<script>
class SlotMonitor extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('event-template');
shadow.appendChild(template.content.cloneNode(true));
const slot = shadow.getElementById('monitored');
const log = shadow.getElementById('log');
slot.addEventListener('slotchange', (e) => {
const nodes = slot.assignedNodes();
const elements = slot.assignedElements();
log.innerHTML =
'<strong>Slot changed!</strong><br>' +
'Assigned nodes: ' + nodes.length + '<br>' +
'Assigned elements: ' + elements.length + '<br>' +
'Content: ' + Array.from(elements)
.map(el => el.textContent)
.join(', ');
});
}
}
customElements.define('slot-monitor', SlotMonitor);
</script>
<slot-monitor id="demo">
<p>Initial content</p>
</slot-monitor>
<button onclick="addContent()">Add Content</button>
<button onclick="removeContent()">Remove Content</button>
<script>
function addContent() {
const demo = document.getElementById('demo');
const p = document.createElement('p');
p.textContent = 'New paragraph ' + (demo.children.length + 1);
demo.appendChild(p);
}
function removeContent() {
const demo = document.getElementById('demo');
if (demo.lastElementChild) {
demo.lastElementChild.remove();
}
}
</script>
<style>
button {
padding: 0.75em 1em;
margin: 0.5em 0.25em;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
Access assigned content programmatically:
Interactive code playground requires JavaScript. Here's the code:
<template id="nodes-template">
<style>
.info {
padding: 1em;
background: #f9fafb;
border-radius: 4px;
}
</style>
<div class="info">
<slot id="content"></slot>
<div id="node-info"></div>
</div>
</template>
<script>
class NodeInfo extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('nodes-template');
shadow.appendChild(template.content.cloneNode(true));
const slot = shadow.getElementById('content');
const info = shadow.getElementById('node-info');
slot.addEventListener('slotchange', () => {
// Get all assigned nodes (including text nodes)
const nodes = slot.assignedNodes();
// Get only element nodes
const elements = slot.assignedElements();
info.innerHTML =
'<hr><strong>Slot Analysis:</strong><br>' +
'Total nodes: ' + nodes.length + '<br>' +
'Element nodes: ' + elements.length + '<br>' +
'Text nodes: ' + (nodes.length - elements.length);
});
}
}
customElements.define('node-info', NodeInfo);
</script>
<node-info>
Text node
<strong>Element 1</strong>
More text
<em>Element 2</em>
</node-info> Interactive code playground requires JavaScript. Here's the code:
<template id="slot-info-template">
<style>
.container { padding: 1em; background: #eff6ff; border-radius: 4px; }
</style>
<div class="container">
<slot name="header"></slot>
<slot></slot>
</div>
</template>
<script>
class SlotInfo extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const template = document.getElementById('slot-info-template');
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
customElements.define('slot-info', SlotInfo);
</script>
<slot-info>
<h3 slot="header" id="heading">My Heading</h3>
<p id="paragraph">My paragraph</p>
</slot-info>
<button onclick="checkSlots()">Check Assigned Slots</button>
<div id="result" style="margin-top: 1em; padding: 1em;
background: #f3f4f6; border-radius: 4px;"></div>
<script>
function checkSlots() {
const heading = document.getElementById('heading');
const paragraph = document.getElementById('paragraph');
const result = document.getElementById('result');
result.innerHTML =
'<strong>Assigned Slots:</strong><br>' +
'Heading slot: ' + (heading.assignedSlot?.name || 'default') + '<br>' +
'Paragraph slot: ' + (paragraph.assignedSlot?.name || 'default');
}
</script>
<style>
button {
padding: 0.75em 1.5em;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 1em;
}
</style>
Interactive code playground requires JavaScript. Here's the code:
<template id="layout-template">
<style>
.layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-columns: 200px 1fr;
grid-template-rows: auto 1fr auto;
gap: 1em;
min-height: 300px;
border: 2px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.header { grid-area: header; background: #3b82f6; color: white; padding: 1em; }
.sidebar { grid-area: sidebar; background: #f3f4f6; padding: 1em; }
.main { grid-area: main; padding: 1em; }
.footer { grid-area: footer; background: #1f2937; color: white; padding: 0.75em 1em; }
</style>
<div class="layout">
<header class="header">
<slot name="header">Header</slot>
</header>
<aside class="sidebar">
<slot name="sidebar">Sidebar</slot>
</aside>
<main class="main">
<slot>Main content</slot>
</main>
<footer class="footer">
<slot name="footer">Footer</slot>
</footer>
</div>
</template>
<script>
class PageLayout extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('layout-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('page-layout', PageLayout);
</script>
<page-layout>
<h1 slot="header">My Application</h1>
<nav slot="sidebar">
<ul style="list-style: none; padding: 0;">
<li>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
</nav>
<article>
<h2>Main Content</h2>
<p>This is the main content area.</p>
</article>
<p slot="footer">© 2025 My Company</p>
</page-layout>
Interactive code playground requires JavaScript. Here's the code:
<template id="dialog-template">
<style>
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
}
:host([open]) .backdrop {
display: flex;
}
.dialog {
background: white;
border-radius: 8px;
min-width: 300px;
max-width: 500px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.title {
padding: 1em 1.5em;
border-bottom: 1px solid #e5e7eb;
font-size: 1.2em;
font-weight: bold;
}
.body {
padding: 1.5em;
}
.actions {
padding: 1em 1.5em;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
gap: 0.5em;
}
</style>
<div class="backdrop">
<div class="dialog">
<div class="title">
<slot name="title">Dialog Title</slot>
</div>
<div class="body">
<slot>Dialog content</slot>
</div>
<div class="actions">
<slot name="actions">
<button>OK</button>
</slot>
</div>
</div>
</div>
</template>
<script>
class CustomDialog extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('dialog-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('custom-dialog', CustomDialog);
</script>
<custom-dialog open>
<span slot="title">Confirm Action</span>
<p>Are you sure you want to proceed?</p>
<div slot="actions">
<button style="padding: 0.5em 1em; background: #e5e7eb; border: none; border-radius: 4px; cursor: pointer;">Cancel</button>
<button style="padding: 0.5em 1em; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;">Confirm</button>
</div>
</custom-dialog>
Excellent Modern Support
The <slot> element is supported in all modern browsers:
Chrome : 53+ (2016)
Firefox : 63+ (2018)
Safari : 10+ (2016)
Edge : 79+ (2020)
Opera : 40+ (2016)
Mobile : Widely supported
Note: Requires Shadow DOM support. Use polyfills for older browsers.
<template> - Content templates
Shadow DOM - Encapsulated DOM trees
Custom Elements - Define new HTML elements
Web Components - Reusable component standard
Provide fallback content : Always include default content in slots
Name slots clearly : Use descriptive names for named slots
Keep slot structure simple : Don’t overcomplicate slot hierarchies
Use ::slotted() carefully : Remember it only styles direct children
Document slots : Clearly document which slots your component accepts
Event handling : Be aware of event retargeting in Shadow DOM
Accessibility : Ensure slotted content maintains accessibility
Test without content : Verify fallback content works correctly
Monitor changes : Use slotchange events when needed
Style defensively : Remember light DOM styles can override
❌ Avoid
Relying on slot content being present
Complex nested slot structures
Overusing named slots
Forgetting ::slotted() limitations
Not testing with empty slots
✅ Do Instead
Provide meaningful fallback content
Keep slot structure flat and simple
Use default slot when possible
Test both with and without content
Document expected slot usage
Learn More: