Intro to HTML for LWC: Templates, Directives, and the Rules That Trip Everyone Up
Session 93 covers the HTML side of Lightning Web Components — the part everyone assumes they already know because they've written HTML before. Most of it transfers directly. The parts that don't are exactly the parts that cause the confusing errors: the single root element rule, the handful of framework directives written as attributes, and the key requirement on every loop. This session covers all of it, with real code.
HTML in LWC Is Real HTML — With One Framework Layer on Top
An LWC template file is standard HTML5. There's no special syntax to learn for tags, attributes, or nesting — a <div> is a div, a <button> is a button, exactly like any HTML you've written before. What makes it an LWC template rather than a static HTML file is a small set of framework-recognized directives written as regular-looking HTML attributes — lwc:if, for:each, and a few others — plus one structural rule the compiler enforces before your component can even build.
Where the Template Lives
Every LWC has its own HTML file, alongside the JavaScript and CSS — myComponent.html, myComponent.js, myComponent.css, same folder, same base name. Everything inside that file has to be wrapped in a single root <template> tag. That's not optional and not stylistic — it's the compiler's entry point for the whole component.
<template>
<div class="container">
<h2>{title}</h2>
<p>Welcome to Lightning Web Components.</p>
</div>
</template>
The Single Root Rule
This is the rule that catches almost everyone the first time. A template can contain as much nested structure as you want, but directly under the <template> tag, there must be exactly one top-level element. Two sibling elements at the root level will fail to compile.
<!-- Invalid — two root-level siblings -->
<template>
<h1>Title</h1>
<p>Body text</p>
</template>
<!-- Valid — one root element wrapping both -->
<template>
<div>
<h1>Title</h1>
<p>Body text</p>
</div>
</template>
Data Binding Basics
Curly braces bind a JavaScript property from your component's class straight into the template. No special import, no directive — just {propertyName}, and it stays in sync with the property automatically.
<!-- myComponent.html -->
<template>
<div class="container">
<p>Hello, {greeting}!</p>
</div>
</template>
// myComponent.js
import { LightningElement } from 'lwc';
export default class MyComponent extends LightningElement {
greeting = 'World';
}
Conditional Rendering: lwc:if, lwc:elseif, lwc:else
Instead of embedding logic with something like a ternary expression the way JSX allows, LWC templates render conditionally through directives written directly on the elements involved — lwc:if, lwc:elseif, and lwc:else.
<template>
<div>
<p lwc:if={isLoading}>Loading records...</p>
<p lwc:elseif={hasError}>Something went wrong.</p>
<p lwc:else>Here are your records.</p>
</div>
</template>
Only one branch renders at a time, evaluated top to bottom exactly like an if/else-if/else chain in JavaScript. This is a fairly recent, more readable replacement for the older if:true / if:false directive pair, which still work but don't support the elseif branch directly.
Why Not Just Use JSX Like React?
This is the question every developer coming from React eventually asks. LWC is built on the W3C Web Components standard, not a proprietary virtual DOM library. Writing templates as real HTML means a compiled LWC becomes a native custom element the browser understands natively — no extra runtime library required to interpret embedded logic, and no dependency on the React ecosystem to run. The tradeoff is that conditional logic and loops live as HTML attributes (directives) instead of inline JavaScript expressions — less flexible in some edge cases, but it keeps components genuinely standards-based and portable.
Loops: for:each
for:each iterates over an array from your JavaScript class, exposing the current item and index through for:item and for:index.
<template>
<ul>
<li for:each={contacts} for:item="contact" key={contact.Id}>
{contact.Name}
</li>
</ul>
</template>
// myComponent.js
contacts = [
{ Id: '1', Name: 'Alex Chen' },
{ Id: '2', Name: 'Priya Nair' },
];
The Key Requirement — the Second Rule That Trips People Up
Every element rendered by for:each needs a key attribute holding a unique, stable value — usually a record ID. Skip it, and LWC throws an error rather than silently guessing which DOM node maps to which array item.
<!-- Missing key — throws at build/runtime:
"Invalid template ... missing key for iteration" -->
<li for:each={contacts} for:item="contact">
{contact.Name}
</li>
<!-- Correct — key is a unique, stable identifier -->
<li for:each={contacts} for:item="contact" key={contact.Id}>
{contact.Name}
</li>
The key isn't cosmetic. The framework uses it to track exactly which rendered node corresponds to which array item across re-renders, so it can update, reorder, or remove precisely the right elements instead of throwing away and rebuilding the whole list every time the underlying data changes.
for:iterator — When You Need First/Last/Index
When you need to know whether the current item is first or last in the list — for adding a divider between rows but not after the last one, for example — for:iterator gives you that directly, without manual index math.
<template>
<template for:iterator="it" iterator:it={contacts}>
<div key={it.value.Id}>
{it.value.Name}
<hr if:false={it.last} />
</div>
</template>
</template>
Event Binding in Templates
DOM events bind to handler methods in your JavaScript class the same way property binding works — curly braces, this time on an on* attribute, pointing to a method name rather than a value.
<template>
<button onclick={handleClick}>Save</button>
</template>
// myComponent.js
handleClick(event) {
console.log('Button clicked');
}
Patterns You'll Rebuild Constantly
A handful of combinations cover most real components: a loading/error/success three-way branch using lwc:if / lwc:elseif / lwc:else. A list with an empty state — for:each over the array, with an lwc:if checking the array's length to show a fallback message when it's empty. And a dynamic class binding, where a computed getter in your JS class returns a string used as the value bound into class={computedClassName}. None of these require anything beyond what's covered here — just these directives combined deliberately.
That's the whole intro: one required root element, curly-brace binding, three conditional directives, two looping directives, and a key requirement that exists for a real technical reason, not a style preference. Get comfortable with these and the rest of LWC template work is mostly composition, not new syntax.
Frequently Asked Questions
Is LWC HTML the same as regular HTML?
Mostly, yes — it's standard HTML5 syntax. The differences are the required root <template> tag wrapping everything, and a small set of framework directives (lwc:if, for:each, lwc:ref, and similar) written as special attributes that the LWC compiler recognizes and turns into efficient DOM operations at build time.
Why does an LWC template need exactly one root element?
Because the compiler needs a single, unambiguous root node to attach the component's shadow DOM boundary to. Everything inside your <template> tag must be wrapped in exactly one top-level element — you can nest as much as you want inside it, but you can't have two sibling elements directly under <template>.
Why does for:each require a key attribute?
The framework uses the key to track which rendered DOM node corresponds to which array item across re-renders, so it can update, reorder, or remove the right elements instead of re-rendering the whole list from scratch. Without a unique key on the iterated element, LWC throws an error at build or runtime rather than guessing.
Why doesn't Salesforce just use JSX like React?
LWC is built on the W3C Web Components standard, not a proprietary virtual DOM library. Using real HTML templates instead of JSX means components compile down to native custom elements the browser understands directly, with no extra runtime library needed to interpret them — lighter weight, and portable outside the React ecosystem entirely.