Template & Slot
The <template> and <slot> elements are foundational Web Components technologies for declarative content reuse and composition. The <template> element holds inert HTML fragments that can be cloned and activated at runtime, while the <slot> element defines insertion points within those fragments for flexible content projection.
Together, they enable powerful patterns: reusable component templates, dynamic rendering without string concatenation, declarative shadow DOM, and content projection similar to Angular's ng-content or React's children prop — but native to the browser.
info
The <template> element holds HTML that is not rendered by the browser. Its content is accessible via the content property, which returns a DocumentFragment. To use the template, clone the fragment with cloneNode(true) and insert it into the live DOM.
| 1 | <!-- Template definition --> |
| 2 | <template id="user-card"> |
| 3 | <article class="user-card"> |
| 4 | <img class="avatar" src="" alt="" width="48" height="48" /> |
| 5 | <div class="info"> |
| 6 | <h3 class="name"></h3> |
| 7 | <p class="role"></p> |
| 8 | </div> |
| 9 | </article> |
| 10 | </template> |
| 11 | |
| 12 | <div id="container"></div> |
| 13 | |
| 14 | <script> |
| 15 | const template = document.getElementById('user-card'); |
| 16 | const container = document.getElementById('container'); |
| 17 | |
| 18 | // Clone the template content |
| 19 | const clone = template.content.cloneNode(true); |
| 20 | |
| 21 | // Populate cloned content |
| 22 | clone.querySelector('.avatar').src = 'avatar.jpg'; |
| 23 | clone.querySelector('.avatar').alt = 'User avatar'; |
| 24 | clone.querySelector('.name').textContent = 'Jane Doe'; |
| 25 | clone.querySelector('.role').textContent = 'Engineer'; |
| 26 | |
| 27 | // Insert into the live DOM |
| 28 | container.appendChild(clone); |
| 29 | </script> |
The content property is a read-only DocumentFragment. It is important to note that cloneNode performs a deep clone of the fragment, copying all child nodes, attributes, and text content. The original template remains unchanged and reusable.
| 1 | const tmpl = document.getElementById('my-template'); |
| 2 | |
| 3 | // Access the inert document fragment |
| 4 | const fragment = tmpl.content; |
| 5 | console.log(fragment.nodeType); // 11 (DocumentFragment) |
| 6 | |
| 7 | // Clone for insertion (deep clone) |
| 8 | const instance1 = tmpl.content.cloneNode(true); |
| 9 | const instance2 = tmpl.content.cloneNode(true); |
| 10 | |
| 11 | // Each instance is independent |
| 12 | instance1.querySelector('h3').textContent = 'Card 1'; |
| 13 | instance2.querySelector('h3').textContent = 'Card 2'; |
| 14 | |
| 15 | document.body.appendChild(instance1); |
| 16 | document.body.appendChild(instance2); |
| 17 | |
| 18 | // The template remains pristine |
| 19 | console.log(tmpl.content.querySelector('h3').textContent); // '' |
best practice
The <slot> element acts as a placeholder inside a shadow tree (or template) that projects light DOM children into specific positions. It enables content composition — the component author defines the structure, while the consumer provides the content.
Default and Named Slots
A slot without a name attribute is the default slot and captures all child nodes not assigned to a named slot. Named slots use the name attribute and match light DOM children that have the corresponding slot attribute.
| 1 | <template id="page-layout"> |
| 2 | <style> |
| 3 | .layout { display: grid; grid-template-rows: auto 1fr auto; min-height: 200px; } |
| 4 | header { background: #111; padding: 16px; border-bottom: 1px solid #333; } |
| 5 | main { padding: 16px; } |
| 6 | footer { background: #111; padding: 12px 16px; border-top: 1px solid #333; font-size: 12px; color: #666; } |
| 7 | </style> |
| 8 | <div class="layout"> |
| 9 | <header><slot name="header">Header</slot></header> |
| 10 | <main><slot>Main Content</slot></main> |
| 11 | <footer><slot name="footer">Footer</slot></footer> |
| 12 | </div> |
| 13 | </template> |
| 14 | |
| 15 | <page-layout> |
| 16 | <h1 slot="header">My Page</h1> |
| 17 | <p>This is the main content area.</p> |
| 18 | <p slot="footer">© 2026 Blueprint Docs</p> |
| 19 | </page-layout> |
Fallback Content
Slot fallback content renders when no light DOM node is assigned to the slot. The fallback lives inside the slot element in the shadow tree and is replaced when the consumer provides content.
| 1 | <template id="tooltip-icon"> |
| 2 | <style> |
| 3 | .tooltip { position: relative; display: inline-flex; cursor: help; } |
| 4 | .tip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); |
| 5 | background: #333; color: #fff; padding: 4px 8px; border-radius: 4px; |
| 6 | font-size: 11px; white-space: nowrap; display: none; } |
| 7 | .tooltip:hover .tip { display: block; } |
| 8 | </style> |
| 9 | <span class="tooltip"> |
| 10 | <slot name="icon">ⓘ</slot> |
| 11 | <span class="tip"><slot>Info</slot></span> |
| 12 | </span> |
| 13 | </template> |
| 14 | |
| 15 | <!-- Uses fallback content --> |
| 16 | <tooltip-icon></tooltip-icon> |
| 17 | |
| 18 | <!-- Custom content replaces fallback --> |
| 19 | <tooltip-icon> |
| 20 | <span slot="icon">⚠</span> |
| 21 | Warning: Disk space low |
| 22 | </tooltip-icon> |
The real power emerges when templates and slots are combined inside custom elements with shadow DOM. The template defines the internal structure and styling, while slots provide the content projection API.
| 1 | <template id="accordion-item"> |
| 2 | <style> |
| 3 | .accordion { border: 1px solid #222; border-radius: 6px; overflow: hidden; margin-bottom: 8px; } |
| 4 | .trigger { width: 100%; padding: 12px 16px; background: #111; border: none; color: #00FF41; cursor: pointer; text-align: left; font-size: 13px; display: flex; justify-content: space-between; } |
| 5 | .trigger:hover { background: #1A1A1A; } |
| 6 | .content { padding: 0 16px; max-height: 0; overflow: hidden; transition: max-height 0.2s; } |
| 7 | .content.open { max-height: 200px; padding: 12px 16px; } |
| 8 | .arrow { transition: transform 0.2s; } |
| 9 | .arrow.open { transform: rotate(180deg); } |
| 10 | </style> |
| 11 | <div class="accordion"> |
| 12 | <button class="trigger" part="trigger"> |
| 13 | <slot name="title">Section</slot> |
| 14 | <span class="arrow" part="arrow">▼</span> |
| 15 | </button> |
| 16 | <div class="content" part="content"> |
| 17 | <slot></slot> |
| 18 | </div> |
| 19 | </div> |
| 20 | </template> |
| 21 | |
| 22 | <accordion-item> |
| 23 | <span slot="title">What is Shadow DOM?</span> |
| 24 | <p>Shadow DOM provides encapsulation for web components.</p> |
| 25 | </accordion-item> |
pro tip
Declarative Shadow DOM (DSD) allows you to create shadow roots directly in HTML without JavaScript. Use a <template> with the shadowrootmode attribute set to open or closed. This enables server-side rendering of web components and reduces JavaScript dependency.
| 1 | <!-- Declarative Shadow DOM — no JS needed --> |
| 2 | <my-component> |
| 3 | <template shadowrootmode="open"> |
| 4 | <style> |
| 5 | :host { display: block; padding: 16px; background: #111; |
| 6 | border: 1px solid #333; border-radius: 8px; } |
| 7 | h2 { color: #00FF41; margin: 0 0 8px; } |
| 8 | slot { color: #A0A0A0; font-size: 13px; } |
| 9 | </style> |
| 10 | <h2>Declarative Component</h2> |
| 11 | <slot>Default content</slot> |
| 12 | </template> |
| 13 | <p>This content is projected into the slot.</p> |
| 14 | </my-component> |
| 15 | |
| 16 | <script> |
| 17 | // DSD polyfill for browsers that don't support it |
| 18 | if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) { |
| 19 | const supportsDSD = document.currentScript?.getAttribute('shadowrootmode'); |
| 20 | if (!supportsDSD) { |
| 21 | // Apply polyfill (e.g., @webcomponents/template-shadowroot) |
| 22 | } |
| 23 | } |
| 24 | </script> |
Declarative Shadow DOM is supported in Chromium-based browsers and Safari 16.4+. For Firefox and older browsers, a lightweight polyfill is available from the webcomponents project.
| 1 | <!-- Multiple declarative shadow components --> |
| 2 | <style> |
| 3 | .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; max-width: 600px; margin: 0 auto; } |
| 4 | </style> |
| 5 | <div class="grid"> |
| 6 | <div> |
| 7 | <template shadowrootmode="open"> |
| 8 | <style> :host { display: block; background: #0A0A0A; border: 1px solid #333; border-radius: 6px; padding: 12px; font-family: system-ui; } h3 { margin: 0 0 4px; color: #00FF41; font-size: 14px; } p { margin: 0; font-size: 12px; color: #808080; } </style> |
| 9 | <h3>CPU Usage</h3> |
| 10 | <p>Idle — 2% utilization</p> |
| 11 | </template> |
| 12 | </div> |
| 13 | <div> |
| 14 | <template shadowrootmode="open"> |
| 15 | <style> :host { display: block; background: #0A0A0A; border: 1px solid #333; border-radius: 6px; padding: 12px; font-family: system-ui; } h3 { margin: 0 0 4px; color: #00FF41; font-size: 14px; } p { margin: 0; font-size: 12px; color: #808080; } </style> |
| 16 | <h3>Memory</h3> |
| 17 | <p>6.2 GB / 16 GB used</p> |
| 18 | </template> |
| 19 | </div> |
| 20 | </div> |
warning
Template instantiation refers to patterns for efficiently creating multiple instances from a single template. The recommended approach is to clone the fragment once per instance, populate it with data, and append it to the DOM. This is significantly faster than parsing innerHTML for each instance.
| 1 | const tmpl = document.getElementById('item-template'); |
| 2 | const list = document.getElementById('list'); |
| 3 | |
| 4 | const items = [ |
| 5 | { name: 'Alice', role: 'Developer' }, |
| 6 | { name: 'Bob', role: 'Designer' }, |
| 7 | { name: 'Carol', role: 'Manager' }, |
| 8 | ]; |
| 9 | |
| 10 | // Batch create from template — efficient |
| 11 | const fragment = document.createDocumentFragment(); |
| 12 | |
| 13 | for (const item of items) { |
| 14 | const clone = tmpl.content.cloneNode(true); |
| 15 | clone.querySelector('.name').textContent = item.name; |
| 16 | clone.querySelector('.role').textContent = item.role; |
| 17 | fragment.appendChild(clone); |
| 18 | } |
| 19 | |
| 20 | // Single append for performance |
| 21 | list.appendChild(fragment); |
For very large datasets (thousands of items), consider using a virtual scroller or pagination alongside template cloning. Each clone is a lightweight document fragment, but excessive DOM nodes can still impact rendering performance.
Templates and slots serve a wide range of use cases across web development.
Live preview of a template-powered card list with slots:
cloneNode vs innerHTML
Cloning a template with cloneNode(true) is consistently 2-5x faster than parsing the same HTML string with innerHTML. Additionally, cloneNode preserves event listeners bound to elements in the template, while innerHTML requires re-attaching them. For any scenario where you render the same structure multiple times, templates are the clear winner.
| Approach | Performance | Event Listeners | Markup Source |
|---|---|---|---|
| template + cloneNode | Fast — pre-parsed fragment | Preserved | Single source of truth |
| innerHTML | Slower — re-parses each time | Lost — must re-attach | String concatenation |
| createElement | Moderate — many API calls | Preserved | Imperative code |
Fragment Performance
best practice
note