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:if directly on that element
  • Multiple elements: When controlling multiple elements, wrap them in a <template> tag with lwc: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:if directly 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:key with lwc:for for 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:ref instead of querySelector when possible for better performance
  • Access refs in renderedCallback or 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 id values 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 slotchange when 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