All insights

Platform

Complex template expressions in LWC: deleting getters you only wrote to format values

Winter '27 makes complex template expressions generally available in Lightning Web Components. Set apiVersion to 66.0 and a real subset of JavaScript runs in the template, so the getters that existed only to build a CSS class or format a date can go. This is what's allowed, the double-quote rule that trips the compiler, and where a getter still earns its place.

Complex template expressions in LWC: deleting getters you only wrote to format values, article illustration

Open any mature LWC component and count the getters that exist only to feed the template. A get formattedDate() that wraps one Intl call. A get rowClass() that stitches together two CSS class names based on a boolean. A get hasItems() that returns this.items.length > 0. None of them hold logic worth its own method. They exist because, for years, the LWC template could read a property and nothing else.

Winter ‘27 makes complex template expressions generally available, and those getters can go. The feature shipped in beta in Spring ‘26 and is GA now. Set your component’s apiVersion to 66.0 or later and a broad subset of JavaScript runs directly in the template’s curly braces.

My take before the detail: use this to delete display-only getters and let the template say what it shows. Do not use it to move real logic into markup. The line between those two is the whole skill, and the compiler won’t draw it for you.

What you can now write in the braces

Before, {property} was the ceiling. A bare property reference, or a getter that pretended to be one. Now the braces take a comprehensive subset of standard JavaScript expressions: arithmetic, comparisons, ternaries, string concatenation, property and array access, logical operators. No custom syntax, no new templating language. It’s JavaScript you already know, evaluated where the value is used.

The mechanical win is obvious the moment you see the two side by side. The old shape first, a component carrying getters that do nothing but format:

// invoiceCard.js, the getter-per-display-value pattern
import { LightningElement, api } from 'lwc';

export default class InvoiceCard extends LightningElement {
    @api invoice;

    get formattedTotal() {
        return `$${this.invoice.total.toFixed(2)}`;
    }

    get isOverdue() {
        return this.invoice.dueDate < Date.now();
    }

    get statusClass() {
        return this.invoice.dueDate < Date.now()
            ? 'slds-text-color_error'
            : 'slds-text-color_success';
    }
}
<!-- invoiceCard.html -->
<template>
    <p class={statusClass}>{formattedTotal}</p>
    <template lwc:if={isOverdue}>
        <span>Overdue</span>
    </template>
</template>

With apiVersion at 66.0, the same component drops all three getters and the template carries the intent:

<!-- invoiceCard.html, apiVersion 66.0+ -->
<template>
    <p class="{invoice.dueDate < Date.now()
        ? 'slds-text-color_error'
        : 'slds-text-color_success'}">
        {formattedTotal}
    </p>
    <template lwc:if={invoice.dueDate < Date.now()}>
        <span>Overdue</span>
    </template>
</template>

The JavaScript file loses two of its three getters. formattedTotal stays, because currency formatting calls a method and that belongs in JavaScript, a boundary I’ll come back to. The display-only boolean and the class assembly move into the template, where anyone reading it sees what renders without decoding a getter name.

The double-quote rule that will fail your first build

Here is the detail that costs everyone an hour the first time. A complex expression used in an attribute has to be wrapped in double quotes. Miss the quotes and the LWC compiler throws, and the error doesn’t always point at the real cause.

<!-- Wrong: bare complex expression in an attribute, compiler error -->
<div class={isActive ? 'active' : 'inactive'}></div>

<!-- Right: the whole expression wrapped in double quotes -->
<div class="{isActive ? 'active' : 'inactive'}"></div>

A bare property in an attribute, class={someClass}, still works the way it always did. The quotes are required specifically once the expression contains operators, a ternary, or anything beyond a single property reference. The rule of thumb that keeps you out of trouble: quote any attribute expression that has an operator in it, and you’ll never hit the error.

Text-node expressions between tags, the {...} sitting as element content, don’t take the quotes. It’s only attributes. That asymmetry is the part worth memorising.

It compiles ahead of time, so there’s no runtime tax

A fair worry with logic in templates is performance. Complex template expressions are processed at compile time, not evaluated by some runtime engine on each cycle, so there’s no interpreter running on every render.

The reactivity model is unchanged, the expression tracks the same properties it references, and re-renders fire on the same rules as before. It stays inside the Lightning security sandbox too, so nothing about the LWC security model loosens.

That’s the reason this is a readability change and not a performance trade. You’re not choosing between a clean template and a fast one. The compiled output is equivalent to the getter you deleted.

Where a getter still earns its place

Now the discipline, because “you can put JavaScript in the template” is exactly the kind of freedom that rots a codebase if you take it literally.

Keep it in the JavaScript file when the expression does any of these:

  • Calls a method that isn’t a plain, safe accessor. Method invocation in templates has real limits, and some calls simply aren’t allowed, so anything beyond a trivial built-in belongs in a getter.
  • Needs an intermediate variable, or runs past one line. If you’d want a const to make it readable, the template is the wrong home.
  • Repeats across the template. A CSS-class expression used in five places is a getter, so you change it once.
  • Encodes a business rule. “Is this order eligible for expedited shipping” is not display formatting, and burying it in markup hides it from the next developer and from your tests.

Salesforce’s own guidance lands in the same place: quote anything with an operator, and if the expression exceeds a line or needs intermediate variables, don’t put it in the template.

The test I use is simpler. If a teammate reading the template would have to stop and work out what the expression means, it wanted a well-named getter. Template expressions are for the values that are self-evident once you see them, and a getter is for the ones that aren’t.

There’s a testing angle here too. A getter is trivially unit-testable in a Jest spec, and an expression baked into markup is only tested through rendering.

Anything you’d want a direct assertion on, a monetary calculation, an eligibility flag, is better as a getter you can call in isolation. This is the same instinct that keeps technical debt from accumulating: put logic where it can be named and tested, not where it’s merely convenient.

Rolling it out without breaking what works

The feature is opt-in per component, gated on apiVersion 66.0 or later in the component’s .js-meta.xml. Older components keep their current behaviour untouched until you bump the version, so there’s no org-wide switch to flip and nothing breaks on your upgrade weekend. That’s the safe way to adopt it: raise the version on new components, and refactor an existing one only when you’re already in it for another reason.

If your team maintains a shared component library, agree on the boundary before anyone starts deleting getters, or you’ll get one developer inlining three-clause ternaries and another keeping single-property getters.

Write the rule down once. Display-only, single-line, no method calls goes in the template, and everything else stays a getter. It’s the same kind of team convention worth settling that we argued for around the Flow-versus-Apex-versus-Agentforce decision, where the tooling gives you options and the discipline is knowing which one a given job wants.

Complex template expressions won’t change what your components can do. They change how much ceremony it takes to display a value, and they make the template an honest description of the UI instead of a set of pointers into a class full of one-line getters. Adopt it for that, hold the line on real logic, and your components get shorter and clearer at the same time. Reach past that line and you’ve just moved your spaghetti from the .js file into the .html one, where it’s harder to test and harder to find.

Understanding the basics

What are complex template expressions in LWC?

They’re the ability to write a broad subset of standard JavaScript, arithmetic, comparisons, ternaries, string operations and property access, directly inside a Lightning Web Component’s HTML template, rather than only reading a property or a getter. The feature was beta in Spring ‘26 and became generally available in Winter ‘27.

What apiVersion do complex template expressions require?

Set the component’s apiVersion to 66.0 or later in its .js-meta.xml file. It’s opt-in per component, so components on earlier versions keep their existing template behaviour and nothing changes until you raise the version.

Why does my LWC template expression throw a compiler error?

The most common cause is an unquoted complex expression in an attribute. Any attribute expression containing an operator or a ternary must be wrapped in double quotes, for example class="{isActive ? 'a' : 'b'}". A bare single-property reference like class={value} still works without quotes, and text-node expressions between tags don’t need them.

Do complex template expressions hurt performance?

No. They’re processed at compile time rather than evaluated by a runtime engine on each render, so the compiled output is equivalent to the getter you’d have written. Reactivity is unchanged and the expression tracks the same properties it references, and the feature stays inside the LWC security sandbox.


Modernising a component library on Winter ‘27, or untangling a codebase where the logic and the markup drifted apart years ago, is the kind of work we do every day. Talk to us about clean, testable, release-ready Salesforce.

Keep reading

All insights