LWC Templates and Slots
Template Directives
Read template directives in this order: expression rules first, conditional rendering (lwc:if, lwc:elseif, lwc:else), list rendering (lwc:for and lwc:key), then advanced patterns (lwc:ref, lwc:dom="manual").
Template Expression Rules
Important: Brackets ({}) in templates can only reference variables, properties, or getters. Logic and expressions are not allowed in brackets.
Guidelines:
- Allowed: Property references, getter calls, method calls (no parameters)
- Not Allowed: Comparison operators (
===,!==,>,<), logical operators (&&,||), arithmetic operations, ternary operators
Examples:
<!-- Good: Reference property directly -->
<p>{accountName}</p>
<lightning-button lwc:if={isVisible} label="Click Me"></lightning-button>
<!-- Good: Reference getter (getter contains the logic) -->
<!-- JavaScript: get isSuccess() { return this.status === 'success'; } -->
<p lwc:if={isSuccess} role="status">Success</p>
<!-- Good: Reference method call -->
<lightning-button lwc:if={isVisible} label="Click Me" onclick={handleClick}></lightning-button>
<!-- Bad: Logic in brackets (not allowed) -->
<div lwc:if={status === 'success'}>Success</div>
<div lwc:if={items.length > 0}>Has items</div>
<div>{isVisible && accountName}</div>
<div>{count + 1}</div>
Solution: Use getters for computed conditions:
// Bad: Logic in template
// Template: <div lwc:if={status === 'success'}>Success</div>
// Good: Logic in getter
get isSuccess() {
return this.status === 'success';
}
// Template: <div lwc:if={isSuccess}>Success</div>
lwc:if
Usage: Use lwc:if for conditional rendering. This is the modern, preferred approach over if:true/if:false.
Migration Requirement: Any existing if:true/if:false should be migrated to lwc:if when touching the component.
Guidelines:
- Single element: When controlling a single element, place
lwc:ifdirectly on that element - Multiple elements: When controlling multiple elements, wrap them in a
<template>tag withlwc:if - No logic in brackets: Brackets can only reference variables/properties, not expressions. Use getters for computed conditions
Examples:
<!-- Good: Single element - lwc:if on the element itself -->
<lightning-button lwc:if={isVisible} label="Click Me"></lightning-button>
<!-- Good: Single element with lwc:if -->
<p lwc:if={isLoading} class="loading">Loading...</p>
<!-- Good: Multiple elements - wrap in template -->
<template lwc:if={isVisible}>
<p>Content is visible</p>
<p>Additional content</p>
</template>
<!-- Good: Use lwc:else for alternative content -->
<lightning-spinner lwc:if={isLoading}></lightning-spinner>
<template lwc:else>
<p>Content loaded</p>
<p>Additional loaded content</p>
</template>
<!-- Good: Multiple conditions using getters (no logic in brackets) -->
<!-- JavaScript: get isSuccess() { return this.status === 'success'; } -->
<p lwc:if={isSuccess} class="success" role="status">Success message</p>
<!-- JavaScript: get isError() { return this.status === 'error'; } -->
<p lwc:if={isError} class="error" role="alert">Error message</p>
<!-- JavaScript: get isDefault() { return !this.isSuccess && !this.isError; } -->
<p lwc:if={isDefault}>Default message</p>
<!-- Avoid: unnecessary `<template>` wrapper for a single element; put `lwc:if` on the element instead -->
<template lwc:if={isVisible}>
<lightning-button label="Click Me"></lightning-button>
</template>
<!-- Bad: Logic in brackets (not allowed) -->
<div lwc:if={status === 'success'}>Success</div>
<!-- Bad: Old if:true syntax (should be migrated) -->
<div if:true={isVisible}>Content</div>
Performance Note: lwc:if removes elements from the DOM when false, which is more efficient than hiding with CSS.
lwc:else and lwc:elseif
Usage: Use lwc:else and lwc:elseif with lwc:if for alternative content rendering.
Guidelines:
- No logic in brackets: Use getters for computed conditions, not expressions in brackets
- Single element: When controlling a single element, place
lwc:ifdirectly on that element - Multiple elements: Wrap multiple elements in
<template>tags
Examples:
<!-- Good: Single element with lwc:else -->
<lightning-spinner lwc:if={isLoading}></lightning-spinner>
<p lwc:else>Content loaded</p>
<!-- Good: Multiple elements with lwc:else -->
<template lwc:if={isLoading}>
<lightning-spinner></lightning-spinner>
<p>Please wait...</p>
</template>
<p lwc:else>Content loaded</p>
<!-- Good: Multiple conditions using getters (no logic in brackets) -->
<!-- JavaScript: get isSuccess() { return this.status === 'success'; } -->
<!-- JavaScript: get isError() { return this.status === 'error'; } -->
<p lwc:if={isSuccess} class="success" role="status">Success</p>
<p lwc:if={isError} class="error" role="alert">Error</p>
<!-- JavaScript: get isDefault() { return !this.isSuccess && !this.isError; } -->
<p lwc:if={isDefault}>Default</p>
<!-- Bad: Logic in brackets (not allowed) -->
<template lwc:if={status === 'success'}>
<p class="success" role="status">Success</p>
</template>
<template lwc:elseif={status === 'error'}>
<p class="error" role="alert">Error</p>
</template>
lwc:for
Usage: Use lwc:for to iterate over arrays and render lists.
Guidelines:
- Always use
lwc:keywithlwc:forfor performance and correct rendering - Use unique, stable keys (prefer IDs over array indices when possible)
- Handle empty arrays appropriately
Examples:
<!-- Good: Use lwc:for with lwc:key -->
<ul>
<template lwc:for={items} lwc:key={item.id}>
<li>{item.name}</li>
</template>
</ul>
<!-- Good: Use index as key only when items don't have unique IDs -->
<ul>
<template lwc:for={items} lwc:key={index}>
<li>{item.name}</li>
</template>
</ul>
<!-- Good: Handle empty arrays using getter -->
<!-- JavaScript: get hasItems() { return this.items && this.items.length > 0; } -->
<template lwc:if={hasItems}>
<ul>
<template lwc:for={items} lwc:key={item.id}>
<li>{item.name}</li>
</template>
</ul>
</template>
<template lwc:else>
<p>No items found</p>
</template>
<!-- Bad: Logic in brackets (not allowed) -->
<template lwc:if={items.length > 0}>
<template lwc:for={items} lwc:key={item.id}>
<div>{item.name}</div>
</template>
</template>
lwc:key
Usage: Always use lwc:key with lwc:for to help LWC track list items efficiently.
Guidelines:
- Use unique, stable identifiers (prefer record IDs over indices)
- Keys should remain the same for the same item across re-renders
- Don't use array indices as keys if items can be reordered
Examples:
// Good: Use unique ID as key
items = [
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' }
];
// In template:
<ul>
<template lwc:for={items} lwc:key={item.id}>
<li>{item.name}</li>
</template>
</ul>
// Acceptable: Use index only when items are static and won't be reordered
<ul>
<template lwc:for={items} lwc:key={index}>
<li>{item.name}</li>
</template>
</ul>
lwc:ref
Usage: Use lwc:ref to get a reference to a DOM element for programmatic access.
Guidelines:
- Use
lwc:refinstead ofquerySelectorwhen possible for better performance - Access refs in
renderedCallbackor after component is rendered - Check if ref exists before using it
Examples:
<!-- Good: Use lwc:ref for element reference -->
<button lwc:ref="actionButton" onclick={handleClick}>
Click Me
</button>
// Good: Access ref in renderedCallback
renderedCallback() {
const button = this.refs.actionButton;
if (button) {
button.focus();
}
}
// Good: Access ref in method after rendering
@api focus() {
const button = this.refs.actionButton;
if (button) {
button.focus();
}
}
lwc:dom="manual"
Usage: Use lwc:dom="manual" when you need to manually manipulate the DOM, bypassing LWC's rendering system.
Guidelines:
- Use sparingly - prefer declarative templates when possible
- Must manually handle DOM updates, including cleanup
- Typically used for third-party libraries or complex DOM manipulation
Examples:
<!-- Good: Manual DOM manipulation for third-party library -->
<div lwc:dom="manual"></div>
// Good: Manual DOM manipulation
renderedCallback() {
if (this._initialized) {
return;
}
const container = this.template.querySelector('.chart-container');
if (container) {
// Initialize third-party chart library
this.chart = new Chart(container, this.chartConfig);
this._initialized = true;
}
}
disconnectedCallback() {
// Cleanup manual DOM manipulation
if (this.chart) {
this.chart.destroy();
this.chart = null;
}
}
Slot Usage
Pass Markup Into Slots
Use <slot></slot> as a placeholder so a parent can pass markup into a child component. When the child renders, the parent's slotted markup is inserted into the slot.
Important: you cannot pass an Aura component into a slot.
Default Slots
Use default (unnamed) slots when there's a single injection point.
<!-- Child component template -->
<template>
<article class="card">
<header class="card-header">
<slot></slot>
</header>
</article>
</template>
<!-- Parent component usage -->
<c-example-card>
<h2>Card Title</h2>
<p>Card content</p>
</c-example-card>
Named Slots
Use named slots when you need multiple injection points.
<!-- Child component template -->
<template>
<article class="card">
<header class="card-header">
<slot name="header"></slot>
</header>
<section class="card-body">
<slot name="body"></slot>
</section>
<footer class="card-footer">
<slot name="footer"></slot>
</footer>
</article>
</template>
<!-- Parent component usage -->
<c-example-card>
<span slot="header">Header Content</span>
<span slot="body">Body Content</span>
<span slot="footer">Footer Content</span>
</c-example-card>
You can also dynamically set the slot attribute (the value is coerced to a string):
<!-- Parent component usage -->
<c-example-card>
<span slot={dynamicSlotName}>Slotted content</span>
</c-example-card>
Scoped Slots
Scoped slots let a child component provide data to the parent's slot content. This lets the parent render markup using values supplied by the child.
In the child component, use lwc:slot-bind on the <slot> element to expose scoped slot data.
<!-- Child component template -->
<template>
<slot name="item" lwc:slot-bind={slotData}></slot>
</template>
In the parent component, use lwc:slot-data to receive that data as a template variable.
<!-- Parent component usage -->
<c-child>
<template lwc:slot-data="slotData">
<div>{slotData.label}</div>
</template>
</c-child>
Slot Fallback Content
Fallback content renders when no parent markup is provided for a slot.
<!-- Good: Fallback for unnamed slot -->
<template>
<article class="card">
<header class="card-header">
<slot>
<h2>Default Title</h2>
</slot>
</header>
</article>
</template>
Conditional Slot Rendering
If a slot should render conditionally, nest the <slot> in a <template> with lwc:if / lwc:else / lwc:elseif.
<template lwc:if={showHeader}>
<slot name="header"></slot>
</template>
Avoid the legacy if:true / if:false directives for slots because the compiler can warn about duplicate slots.
Accessing Slotted Content
Slotted elements are not inside the child component's shadow tree. That means:
- Use
this.querySelector()/this.querySelectorAll()to find elements passed into slots. - Use
this.template.querySelector()only for elements defined inside the child template. - Don't rely on
idvalues in slot queries (template rendering can transform IDs to be globally unique).
handleSlotChange(event) {
// Query slotted content (not shadow tree)
const firstHeader = this.querySelector('h2');
// ...react to slotted content...
}
slotchange Event
All <slot> elements support slotchange. It fires when the direct children of the slot change (for example, when nodes are appended or removed).
Changes inside the children of the slot do not trigger slotchange.
<slot onslotchange={handleSlotChange}></slot>
handleSlotChange(event) {
const slotEl = event.target;
const assignedNodes = slotEl.assignedNodes({ flatten: true });
// ...use assignedNodes...
}
Slot Best Practices
- Use slots to make components flexible and reusable
- Use named slots for complex layouts with multiple injection points
- Provide meaningful fallback content when it improves UX
- Document what markup is expected in each slot (structure, classes, etc.)
- Use
slotchangewhen you need to react to direct node changes in slot content
Reference:
Change History
Version 1.0 - 2026-03-26
- Added initial version history tracking for this document.
Last Updated: 2026-03-26 Version: 1.0