Advertisement
🦉 Core Concepts

OWL JS Template Syntax – QWeb Directives Reference

OWL uses QWeb — Odoo's XML-based template language — for all component rendering. QWeb directives are XML attributes that start with t- and give the template engine instructions: render this text, loop over these items, show this block conditionally. Unlike JSX (React) or Vue's template syntax, QWeb is processed at compile time into a fast render function. This lesson is your complete reference for every directive you will use in day-to-day OWL development.

⏱️ 25 min read 🎯 Beginner 📅 Updated 2026 👁️ Lesson 2 of 5

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.

JavaScript – template registration
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>
  `;
}
ℹ️
The template scope

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.

XML – t-esc
<!-- 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.

XML – t-raw
<!-- ONLY for trusted, sanitized HTML -->
<div t-raw="props.sanitizedHtmlContent"/>
⚠️
t-raw is an XSS risk

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.

XML – conditional rendering
<!-- 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>
💡
t-else must immediately follow t-if or t-elif

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.

Advertisement

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.

XML – list rendering
<!-- 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"
/>
⚠️
Always use a stable, unique t-key

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:

VariableDescriptionExample (t-as="item")
itemCurrent elementitem.name
item_indexZero-based loop indexitem_index
item_firstTrue for the first elementitem_first
item_lastTrue for the last elementitem_last
item_valueFor Object.entries loops, the valueitem_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.

XML – t-set
<!-- 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.

XML – dynamic attributes
<!-- 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.

JavaScript – t-call example
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.

XML + JavaScript – t-ref quick example
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:

XML – t tag uses
<!-- 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

DirectivePurposeQuick Example
t-escHTML-safe text outputt-esc="props.name"
t-rawRaw HTML output (trusted only)t-raw="props.htmlContent"
t-ifConditional rendert-if="state.show"
t-elifElse-if brancht-elif="x > 5"
t-elseFallback brancht-else=""
t-foreachLoop over array/objectt-foreach="state.items"
t-asLoop variable namet-as="item"
t-keyStable identity for list itemst-key="item.id"
t-setDefine local variablet-set="x" t-value="expr"
t-att-[name]Dynamic attributet-att-href="url"
t-attMultiple dynamic attributest-att="attrObj"
t-att-classConditional CSS classest-att-class="{ active: isOn }"
t-refDOM referencet-ref="inputEl"
t-callInclude another templatet-call="${tmplKey}"
t-on-[event]Event listenert-on-click="handleClick"
t-modelTwo-way input bindingt-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>`;
DirectiveDoes
t-escinsert text, escaped (safe)
t-outinsert raw HTML (only for trusted content)
t-if / t-elseconditional rendering
t-foreach + t-keyloop (key required)
t-att-xdynamic 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:

  1. State: an array of { id, name, category } objects, plus a filter string (default "").
  2. Use t-set to compute filteredItems (or use a getter — both work).
  3. Use t-foreach with t-key="item.id" to render the filtered list.
  4. Use t-if / t-else to show "No results" when the filtered list is empty.
  5. Use t-att-class to highlight items where item.category === 'featured'.
  6. Use t-esc for all text output. Do not use t-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-esc renders text safely (HTML-escaped). Always prefer it over t-raw.
  • t-if / t-elif / t-else provide conditional rendering. Elements with a false condition are removed from the DOM entirely.
  • t-foreach + t-as + t-key is the complete loop trio. Always provide a stable unique t-key.
  • t-set defines a local template variable — useful to extract repeated expressions.
  • t-att-class accepts 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-esc do?
  • What is the difference between t-esc and t-out?
  • How do you set dynamic attributes?
  • How are templates compiled?

FAQ

What is the difference between t-esc and just writing the value in text? +

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.

Can I use JavaScript expressions inside t-if, t-foreach, etc.? +

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;).

Why does t-att-class with an object need boolean values? +

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 }".

Can QWeb templates have comments? +

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.