LWC Conditional Rendering
Use this skill when a Lightning Web Component template must branch between UI states — loading vs ready, permitted vs denied, empty vs list, step 1 vs step 2 — or when a legacy template still uses if:true / if:false and needs to be migrated to the modern lwc:if / lwc:elseif / lwc:else trio. It activates on questions about evaluation rules, getter-backed booleans, lifecycle interactions with renderedCallback and lwc:ref, and the idiom for complex boolean logic.
Before Starting
Gather this context before writing or migrating conditional markup:
- How many mutually exclusive branches are there today, and are they truly exclusive or just coincidentally non-overlapping?
- Is the property controlling the branch already reactive (a
@api,@tracked, or wire-provisioned value), or is it derived from other state? - When the branch changes, must any child state survive the toggle (form input, scroll position, focused cell)? That dictates
lwc:if(re-mount) vs CSS hide (keep state). - Is this template still on legacy
if:true/if:false, and is there a chained pattern (if:true, then siblingif:falsefor the else case) that should becomelwc:if/lwc:else?
Core Concepts
Modern conditional rendering in LWC uses three cooperating directives, getters for anything that is not a single boolean, and a mount/unmount model — not a hide/show model.
The Directive Trio: lwc:if, lwc:elseif, lwc:else
lwc:if={prop} evaluates the expression and, when the result is truthy, renders the element and its subtree. lwc:elseif={other} must immediately follow a sibling lwc:if or another lwc:elseif and is evaluated only when the preceding branch was falsy. lwc:else has no expression — lwc:else={foo} is a parse-time error — and it matches whatever remains. The expression inside lwc:if / lwc:elseif is not a reactive watch: reactivity comes from the underlying property (or getter dependency), not from the directive itself. The directive simply re-evaluates on the next rerender.
Getters Are The Idiom For Computed Booleans
Template expressions in LWC are intentionally limited. You cannot write lwc:if={a && b}, lwc:if={status !== 'error'}, or lwc:if={items.length > 0} unless the component's apiVersion is 66.0 or later and you accept Beta risk (see below). Default practice: put computed logic in a JavaScript getter and reference the getter: get isReady() { return this.status === 'done' && !this.error; }, then lwc:if={isReady}. Getters compose cleanly, are unit-testable, and keep the template readable. Inverting a condition should use lwc:else rather than a negated getter when the negation exists only to flip a single branch.
Complex Template Expressions (Spring '26 Beta, apiVersion 66.0+)
Beginning in Spring '26, components at apiVersion 66.0+ may use complex JavaScript expressions directly in HTML templates — e.g. lwc:if={a && b} without a getter. Salesforce documents this as Beta with an explicit "Do not use complex template expressions in production." warning. Prefer getters for production components; use inline expressions only for pilots where the Beta tradeoff is accepted and documented.
Branches Mount And Unmount — They Do Not Hide
When lwc:if flips from true to false, the DOM subtree is removed — not hidden. Child component instances are destroyed, internal state is lost, disconnectedCallback fires, and lwc:ref to anything inside that subtree becomes undefined. When the branch flips back, a fresh instance is created and renderedCallback fires again, so any side-effect code there must be idempotent. If the UX needs to hide a panel while preserving its state (open filter drawer, partially filled form), use CSS display:none or a class toggle instead. Branches also form their own sub-trees for slot assignment: a slotted element lives inside exactly one branch at a time.
Legacy if:true / if:false Still Work — But Are Discouraged
if:true={prop} and if:false={prop} are the pre-lwc:if API. Salesforce's current guidance says they are no longer recommended, may be removed in the future, and are less performant in chained conditions because they do not share the lwc:if / lwc:elseif short-circuit. Migrating is straightforward and the skill's checker flags every instance.
Common Patterns
Loading / Error / Ready State Machine
When to use: A component fetches data and needs to show a spinner, an error card, or the ready view — exactly one at a time.
How it works: Expose a status property ('loading' | 'error' | 'ready'). Back each state with a getter (isLoading, isError, isReady). In the template chain lwc:if={isLoading} → lwc:elseif={isError} → lwc:else for the ready branch. The lwc:else block has no expression.
Why not the alternative: Three parallel lwc:if blocks rely on the JS to guarantee mutual exclusion, so a bug can render two branches at once. The chained directives make the exclusivity a template-level invariant.
Keep-State Toggle vs Reset-State Toggle
When to use: A panel or drawer opens and closes via a button. Whether to use lwc:if or a CSS class depends on whether the user expects their partial state to survive.
How it works: For "reset every time" (confirmation modals, wizards that restart), use lwc:if={isOpen} — the subtree is fresh on every open. For "preserve my input" (filter drawers, collapsed sections), keep the component mounted and toggle display:none via a computed class getter. Document the choice in a comment so future edits do not regress it.
Why not the alternative: Blindly using lwc:if for a drawer destroys in-progress user input; blindly using CSS hide means stale state leaks between opens.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Simple show/hide of one element | lwc:if={flag} |
Simplest primitive; re-mount is usually desirable |
| Two mutually exclusive branches | lwc:if + lwc:else |
Encodes the exclusivity in the template |
| Three or more mutually exclusive branches | lwc:if + lwc:elseif + lwc:else |
Short-circuits and avoids parallel lwc:if bugs |
| Boolean depends on multiple properties | Getter that returns a boolean | Template expressions are intentionally limited |
Pilot-only inline boolean (a && b) in template |
Complex template expression at apiVersion 66.0+ | Beta — Salesforce warns against production use; getters remain the safe default |
| Panel must preserve in-progress state across toggles | CSS display:none via class getter |
lwc:if re-mounts and loses child state |
Existing template uses chained if:true / if:false |
Migrate to lwc:if / lwc:elseif / lwc:else |
Legacy path is slower and officially discouraged |
Recommended Workflow
- Inventory the branches — list every UI state the component can show, confirm mutual exclusivity, and map each state to a property or getter.
- Decide mount vs hide — for each toggle, ask whether child state must survive. Pick
lwc:iffor reset, CSS hide for preserve. - Move logic into getters — any boolean that is not already a single property becomes a getter with a descriptive name (
isReady,canEdit). - Write the chain — use
lwc:if+lwc:elseif+lwc:elsefor exclusive branches; use standalonelwc:iffor independent toggles. - Check lifecycle assumptions — verify
renderedCallbackinside branches is idempotent and that noconnectedCallbackreaches into alwc:refthat may not exist. - Run the checker —
python3 scripts/check_lwc_conditional_rendering.py --lwc-dir force-app/main/default/lwcflags legacy directives, orphanlwc:elseif, expressions inlwc:if, andlwc:else={...}errors. - Migrate legacy — replace every flagged
if:true/if:falsewith the modern trio in the same PR so the checker stays green.
Review Checklist
- No
if:trueorif:falseremains in any touched template. - Every
lwc:elseifimmediately follows a siblinglwc:ifor anotherlwc:elseif. -
lwc:elseappears with no expression. - No template expression contains
&&,||,!==,>,<, or.length— those live in getters. - Any
lwc:refaccessed from JS is null-checked when the referenced element is inside a conditional branch. -
renderedCallbackinside conditional subtrees is idempotent (guarded withthis._renderedor equivalent). - Toggles that must preserve child state use CSS
display:none, notlwc:if.
Salesforce-Specific Gotchas
lwc:ifunmounts — it does not hide — Flipping to false destroys the subtree, firesdisconnectedCallback, and voids anylwc:refpointing inside. A user's partially filled input is lost unless the parent lifts the state up.renderedCallbackfires again on re-entry — Every time the branch flips back to true, a fresh instance is created andrenderedCallbackruns again. Side-effect code (third-party libs, focus(), one-time measurement) must be guarded for idempotency.lwc:else={foo}is a parse-time error —lwc:elsetakes no expression. Authors migrating from other frameworks frequently try to add one.lwc:elseifmust be a sibling followinglwc:if— Wrapping thelwc:ifin a<div>and puttinglwc:elseifoutside that<div>breaks the chain; thelwc:elseifbecomes orphaned and throws at compile time.- Complex expressions inside
lwc:ifdo not compile —lwc:if={a && b}is not valid. Put the logic in a getter. This is intentional — templates stay declarative and testable. - Legacy
if:true/if:falseare on a removal path — They still compile, but Salesforce's own docs call them "no longer recommended" and flag them as less performant in chained conditions.
Output Artifacts
| Artifact | Description |
|---|---|
| Updated template | lwc:if / lwc:elseif / lwc:else chain with getter-backed booleans |
| Migration notes | Before/after snippets converting if:true / if:false to the modern trio |
| Checker report | File-and-line findings for legacy directives, orphan lwc:elseif, complex expressions, and lwc:else={...} errors |
Related Skills
lwc/lwc-performance— use when the core issue is rerender cost, list size, or lazy instantiation beyond the single-branch toggle.lwc/lwc-dynamic-components— use when the runtime choice is which component class to instantiate, not which sub-tree to render.lwc/lwc-template-refs— use when the focus islwc:refcorrectness, especially across mount/unmount boundaries created bylwc:if.