How QWeb Templates Work
A QWeb template is an XML string defined with the xml`` tagged template literal. OWL compiles it once into a JavaScript render function. At render time that function produces a virtual DOM tree, which OWL diffs against the previous tree to update the real DOM efficiently.
import { Component, xml } from "@odoo/owl";
class MyComp extends Component {
// xml`` registers and compiles the template
static template = xml`
<div class="my-comp">
Hello, <span t-esc="props.name"/>!
</div>
`;
}
Inside a template, the component instance is the implicit this. You access props, state, computed getters, and methods directly — without writing this.. The template engine injects these from the component instance at render time.
Text Output: t-esc and t-raw
t-esc — Safe HTML-escaped output
Use t-esc for all text content. It HTML-escapes the value, preventing XSS attacks.
<!-- Render a prop value -->
<span t-esc="props.username"/>
<!-- Render a state value -->
<p t-esc="state.count"/>
<!-- Any JS expression -->
<span t-esc="props.price.toFixed(2) + ' USD'"/>
<!-- Inside text content using t tag -->
<p>Hello, <t t-esc="props.name"/>! You have <t t-esc="state.messages.length"/> messages.</p>
t-raw — Raw HTML output (use with caution)
t-raw renders the value as raw HTML without escaping. Only use it when you own and trust the HTML source (e.g., sanitized content from your own backend). Never use it with user-generated content.
<!-- ONLY for trusted, sanitized HTML -->
<div t-raw="props.sanitizedHtmlContent"/>
Never use t-raw with user input or any string you did not generate and sanitize yourself. If the value contains <script>...</script>, it will execute. Prefer t-esc in all cases unless you have a specific need for raw HTML from a trusted source.
Conditional Rendering: t-if, t-elif, t-else
These work exactly like an if/else if/else chain. The element (and all its children) is only rendered when the condition is truthy.
<!-- Basic t-if -->
<p t-if="state.isLoading">Loading…</p>
<!-- t-if / t-else -->
<div t-if="props.user">
Welcome, <t t-esc="props.user.name"/>
</div>
<div t-else="">Please log in.</div>
<!-- t-if / t-elif / t-else -->
<span t-if="props.score >= 90">A</span>
<span t-elif="props.score >= 80">B</span>
<span t-elif="props.score >= 70">C</span>
<span t-else="">F</span>
<!-- Conditional on <t> (no DOM output, just structural) -->
<t t-if="state.hasError">
<div class="alert"><t t-esc="state.errorMessage"/></div>
</t>
The t-else element must be the next sibling of the t-if or t-elif element in the XML. No other elements or whitespace-only text nodes may appear between them.
List Rendering: t-foreach, t-as, t-key
These three directives always appear together. t-foreach specifies the iterable, t-as names the loop variable, and t-key provides the unique identifier for each item.
<!-- Basic array loop — t-key should be a stable unique value -->
<ul>
<li t-foreach="state.items" t-as="item" t-key="item.id">
<t t-esc="item.name"/>
</li>
</ul>
<!-- Loop over numbers (0..4) -->
<div t-foreach="[0,1,2,3,4]" t-as="n" t-key="n">
<t t-esc="n"/>
</div>
<!-- Access loop index with _index suffix -->
<li t-foreach="props.tags" t-as="tag" t-key="tag_index">
#<t t-esc="tag_index + 1"/>: <t t-esc="tag"/>
</li>
<!-- Loop on a sub-component -->
<ProductCard
t-foreach="state.products"
t-as="product"
t-key="product.id"
name="product.name"
price="product.price"
/>
OWL uses t-key to match old vdom nodes to new ones during re-renders. Using _index as the key when items can be deleted or reordered causes subtle bugs — OWL reuses DOM nodes incorrectly. Use a real unique identifier from your data (like item.id) whenever possible.
Special Variables in t-foreach
Inside a t-foreach loop, OWL automatically provides helper variables using the t-as name as a prefix:
| Variable | Description | Example (t-as="item") |
|---|---|---|
item | Current element | item.name |
item_index | Zero-based loop index | item_index |
item_first | True for the first element | item_first |
item_last | True for the last element | item_last |
item_value | For Object.entries loops, the value | item_value |
Variable Definition: t-set
t-set defines a local template variable, scoped to the current block. Useful for extracting repeated expressions or computed values.
<!-- Define a variable with t-value (JS expression) -->
<t t-set="fullName" t-value="props.firstName + ' ' + props.lastName"/>
<span t-esc="fullName"/>
<!-- Define a variable with body (renders to HTML string) -->
<t t-set="badge">
<span class="badge">Admin</span>
</t>
<div><t t-raw="badge"/></div>
<!-- Useful for DRY: compute once, use multiple times -->
<t t-set="isExpired" t-value="props.expiryDate < Date.now()"/>
<span t-if="isExpired" class="expired-badge">Expired</span>
<p t-if="isExpired">This offer has expired.</p>
Dynamic Attributes: t-att and t-att-*
Use t-att-[attrname] to set a single attribute to a JavaScript expression. Use t-att to set multiple attributes from an object.
<!-- t-att-[name]: single dynamic attribute -->
<img t-att-src="props.imageUrl" t-att-alt="props.imageAlt"/>
<a t-att-href="'/users/' + props.userId">View profile</a>
<input t-att-disabled="state.isLoading"/>
<!-- t-att-class: conditional classes (object syntax) -->
<div t-att-class="{
'card': true,
'card--active': state.isActive,
'card--error': state.hasError,
'card--loading': state.isLoading
}"></div>
<!-- t-att-class: string concatenation -->
<span t-att-class="'badge badge--' + props.color"/>
<!-- t-att: spread multiple attributes from an object -->
<input t-att="{ type: 'text', placeholder: props.placeholder, disabled: state.isDisabled }"/>
Template Reuse: t-call
t-call renders another named template inline. Useful for sharing template fragments across components without creating a full child component.
import { Component, xml } from "@odoo/owl";
// A reusable template fragment
const loadingTemplate = xml`
<div class="loading-spinner">
<span>Loading…</span>
</div>
`;
class Dashboard extends Component {
static template = xml`
<div>
<t t-if="state.isLoading">
<t t-call="${loadingTemplate}"/>
</t>
<t t-else="">
<h1>Dashboard Content</h1>
</t>
</div>
`;
}
DOM References: t-ref
t-ref gives a named reference to a real DOM element. Access it in methods via this.refs.myName. Covered fully in the Refs lesson.
class SearchBox extends Component {
static template = xml`
<div>
<input t-ref="searchInput" type="text" placeholder="Search…"/>
<button t-on-click="focusInput">Focus</button>
</div>
`;
focusInput() {
this.refs.searchInput.focus(); // direct DOM access
}
}
The <t> Structural Tag
The <t> tag is a virtual node — it produces no DOM output. Use it to apply a directive without adding a wrapper element, or to group multiple elements under a single conditional or loop:
<!-- Conditional without a wrapper element -->
<t t-if="state.isAdmin">
<button>Delete</button>
<button>Edit</button>
</t>
<!-- Interpolation inside text -->
<p>You have <t t-esc="state.count"/> unread messages.</p>
<!-- t-set without rendering anything -->
<t t-set="greeting" t-value="'Hello, ' + props.name"/>
QWeb Directives Quick Reference
| Directive | Purpose | Quick Example |
|---|---|---|
t-esc | HTML-safe text output | t-esc="props.name" |
t-raw | Raw HTML output (trusted only) | t-raw="props.htmlContent" |
t-if | Conditional render | t-if="state.show" |
t-elif | Else-if branch | t-elif="x > 5" |
t-else | Fallback branch | t-else="" |
t-foreach | Loop over array/object | t-foreach="state.items" |
t-as | Loop variable name | t-as="item" |
t-key | Stable identity for list items | t-key="item.id" |
t-set | Define local variable | t-set="x" t-value="expr" |
t-att-[name] | Dynamic attribute | t-att-href="url" |
t-att | Multiple dynamic attributes | t-att="attrObj" |
t-att-class | Conditional CSS classes | t-att-class="{ active: isOn }" |
t-ref | DOM reference | t-ref="inputEl" |
t-call | Include another template | t-call="${tmplKey}" |
t-on-[event] | Event listener | t-on-click="handleClick" |
t-model | Two-way input binding | t-model="state.value" |
OWL Templates: QWeb Directives in the DOM
OWL templates are XML using QWeb directives (t- attributes) to bind data, loop, and branch. They compile to fast DOM-updating functions. The expressions read from the component instance — state, props, and your methods.
static template = xml`
<div>
<p t-if="state.loggedIn">Welcome, <t t-esc="state.name"/></p>
<p t-else="">Please log in</p>
<button t-att-disabled="state.busy" t-on-click="save">Save</button>
<li t-foreach="state.items" t-as="item" t-key="item.id">
<t t-esc="item.label"/>
</li>
</div>`;
| Directive | Does |
|---|---|
t-esc | insert text, escaped (safe) |
t-out | insert raw HTML (only for trusted content) |
t-if / t-else | conditional rendering |
t-foreach + t-key | loop (key required) |
t-att-x | dynamic attribute |
Two rules people hit: a template must have a single root element — wrap siblings in a <div> or <t>. And prefer t-esc over t-out: t-esc escapes HTML and blocks injection, while t-out renders raw markup and can open an XSS hole if the data isn't trusted.
🏋️ Practical Exercise
Build a FilterableList component that uses all the key directives from this lesson:
- State: an array of
{ id, name, category }objects, plus afilterstring (default""). - Use
t-setto computefilteredItems(or use a getter — both work). - Use
t-foreachwitht-key="item.id"to render the filtered list. - Use
t-if / t-elseto show "No results" when the filtered list is empty. - Use
t-att-classto highlight items whereitem.category === 'featured'. - Use
t-escfor all text output. Do not uset-raw.
🔥 Challenge Exercise
Write an OWL/QWeb template that interpolates data (t-esc), sets a dynamic attribute (t-att-class), and renders raw HTML safely (t-out). Explain the difference between t-esc and t-out and why escaping matters.
📋 Summary
xml``is a tagged template literal that compiles and registers the QWeb template string. Always use it — never a plain string.t-escrenders text safely (HTML-escaped). Always prefer it overt-raw.t-if / t-elif / t-elseprovide conditional rendering. Elements with a false condition are removed from the DOM entirely.t-foreach+t-as+t-keyis the complete loop trio. Always provide a stable uniquet-key.t-setdefines a local template variable — useful to extract repeated expressions.t-att-classaccepts an object of{ className: boolExpr }for conditional classes.- The
<t>tag is structural — it applies directives without producing a DOM node.
Interview Questions
- What templating language does OWL use?
- What does
t-escdo? - What is the difference between
t-escandt-out? - How do you set dynamic attributes?
- How are templates compiled?
Related Topics
FAQ
Plain text in a QWeb template is treated as a literal string, not a JavaScript expression. If you write <span>props.name</span>, the browser renders the string "props.name". To render the value of props.name, you must use a t-esc attribute or wrap it in a <t t-esc="props.name"/> tag. Think of t-esc as the QWeb equivalent of {{ props.name }} in Vue or {props.name} in JSX.
Yes — the attribute values of all t-* directives are JavaScript expressions evaluated in the component's scope. You can use ternary operators, method calls, array methods, template literals (with the inner quotes), logical operators, comparisons, and so on. The only restriction is that it must be a single expression, not a statement (no let x = 5; return x;).
OWL applies a class name only when its value is truthy. The object form { 'active': state.isActive, 'error': hasError } lets you control multiple independent classes in one expression. OWL merges the object with any static class attribute on the same element, so you can mix static and dynamic classes: class="btn" t-att-class="{ 'btn--primary': isPrimary }".
Yes — standard XML comments <!-- ... --> work inside QWeb templates. They are stripped during compilation and do not appear in the rendered HTML. Use them freely to document complex template sections.

