Advertisement
🦉 Dynamic Templates

OWL JS Conditional Rendering – t-if, t-elif, t-else

Conditional rendering lets you show different parts of your UI based on state, props, or computed values. In OWL JS, this is done entirely with the t-if, t-elif, and t-else directives in the QWeb template. Unlike setting display: none, these directives completely remove elements from the DOM when their condition is false — which has important implications for performance, component lifecycle, and accessibility. This lesson covers every conditional rendering pattern you will need.

⏱️ 16 min read 🎯 Beginner 📅 Updated 2026 👁️ Lesson 1 of 5

t-if Basics

Place t-if on any element. When the expression is falsy, the element and all its children are removed from the DOM. When truthy, they are rendered normally.

XML – basic t-if
<!-- Show alert only when there is an error -->
<div class="alert alert-error" t-if="state.errorMessage">
  <t t-esc="state.errorMessage"/>
</div>

<!-- Show admin panel only for admins -->
<div class="admin-panel" t-if="props.user.isAdmin">
  <AdminControls/>
</div>

<!-- Any JS expression works -->
<p t-if="state.items.length === 0">No items yet.</p>
<p t-if="props.score >= 60">Passed!</p>
<p t-if="!state.isLoading && state.data">Ready.</p>

t-if / t-else

The t-else element is rendered only when the immediately preceding t-if (or t-elif) was false. It must be the immediate next sibling in the template.

XML – t-if / t-else
<!-- Show login button or user name -->
<div t-if="props.user">
  Welcome, <strong t-esc="props.user.name"/>!
</div>
<button t-else="" t-on-click="login">Log In</button>

<!-- Loading / error / content pattern -->
<div t-if="state.isLoading" class="spinner">Loading…</div>
<div t-elif="state.error" class="error" t-esc="state.error"/>
<div t-else="">
  <!-- main content here -->
</div>
⚠️
t-else="" requires the empty string attribute value

Write t-else="" with the empty string — not just t-else (no value). QWeb requires an attribute value even for valueless directives like t-else. Forgetting the ="" causes a template compilation error.

t-if / t-elif / t-else Chains

XML – multi-branch conditional
<!-- Status badge with 4 states -->
<span t-if="props.status === 'active'"    class="badge badge-green">Active</span>
<span t-elif="props.status === 'pending'" class="badge badge-yellow">Pending</span>
<span t-elif="props.status === 'blocked'" class="badge badge-red">Blocked</span>
<span t-else=""                           class="badge badge-gray">Unknown</span>

<!-- Grade calculator -->
<p t-if="props.score >= 90">Grade: A</p>
<p t-elif="props.score >= 80">Grade: B</p>
<p t-elif="props.score >= 70">Grade: C</p>
<p t-elif="props.score >= 60">Grade: D</p>
<p t-else="">Grade: F</p>

Using <t> for Grouping Without a Wrapper

The structural <t> tag lets you conditionally show multiple sibling elements without wrapping them in an extra <div>:

XML – t-if on <t> structural tag
<!-- Show two elements together without adding a DOM wrapper -->
<t t-if="props.user.isAdmin">
  <button class="btn-danger">Delete All</button>
  <button class="btn-warning">Export Data</button>
</t>

<!-- Conditional text inline without disrupting layout -->
<span>
  Hello, <t t-esc="props.name"/>
  <t t-if="props.isNew"> (new user)</t>!
</span>
Advertisement

DOM Removal vs CSS Hiding

This is the most important distinction about t-if. It is fundamentally different from display: none:

Aspectt-if (false)CSS display:none
Element in DOM?❌ Completely removed✅ Present but invisible
Child components mounted?❌ Not mounted — lifecycle hooks don't run✅ Mounted, running
Readable by screen readers?❌ No❌ No (both invisible)
Findable by querySelector?❌ No✅ Yes
Toggle costHigher — creates/destroys DOM + lifecycleLower — just CSS property change
Best forRarely shown content, expensive childrenFrequent show/hide, preserve state
XML – CSS hide vs t-if: choosing the right one
<!-- ✅ t-if: content is rarely shown or expensive to keep alive -->
<VideoPlayer t-if="state.videoVisible" src="props.videoUrl"/>
<!-- VideoPlayer only mounts when visible — no resource usage when hidden -->

<!-- ✅ CSS hide: content toggles frequently or must preserve scroll/state -->
<div t-att-class="{ 'panel': true, 'panel--hidden': !state.panelOpen }">
  <textarea t-model="state.draftText"/>
  <!-- Draft text preserved even when panel closes -->
</div>

Nested Conditionals

XML – nested t-if
<div t-if="props.user">

  <!-- Outer: user exists. Inner: check role -->
  <div t-if="props.user.role === 'admin'">
    <h2>Admin Dashboard</h2>
    <t t-if="props.user.hasApiAccess">
      <button>API Console</button>
    </t>
  </div>

  <div t-elif="props.user.role === 'editor'">
    <h2>Editor Panel</h2>
  </div>

  <div t-else="">
    <h2>Viewer Mode</h2>
  </div>

</div>
<div t-else="">
  <p>Please log in to continue.</p>
</div>

Using t-key to Force Component Remount

Changing a t-key value on a component forces OWL to unmount the old instance and mount a fresh one — useful when you need to completely reset a component's state:

XML – t-key to force remount
<!-- Changing selectedId changes the key → fresh component instance -->
<ProductEditor
  t-key="state.selectedId"
  productId="state.selectedId"
/>
<!-- When user selects a different product, the editor component remounts
     from scratch — onWillStart re-runs, all local state resets -->

Safe Guard Patterns

XML – guarding against null/undefined
<!-- Guard before accessing nested property -->
<p t-if="state.user && state.user.address">
  <t t-esc="state.user.address.city"/>
</p>

<!-- Optional chaining in expression -->
<p t-if="state.user?.address?.city" t-esc="state.user.address.city"/>

<!-- Show count badge only when count > 0 -->
<span t-if="state.unreadCount > 0" class="badge" t-esc="state.unreadCount"/>

t-if / t-else: Conditional Rendering Done Right

t-if renders an element only when its expression is truthy — and crucially, when false the element is removed from the DOM entirely, not just hidden. Chain with t-elif and t-else for branches.

static template = xml`
  <div>
    <p t-if="state.status === 'loading'">Loading…</p>
    <p t-elif="state.error">Failed: <t t-esc="state.error"/></p>
    <ul t-else="">
      <li t-foreach="state.items" t-as="i" t-key="i.id"><t t-esc="i.name"/></li>
    </ul>
  </div>`;

Remove vs hide

t-if falseCSS display:none
In the DOM?no — removedyes — hidden
Component statedestroyed (setup re-runs on return)preserved
Best whenrarely shown / expensivetoggled often

The consequence: a component inside a false t-if is unmounted — its state is thrown away and rebuilt (setup runs again) when the condition flips back. For something toggled constantly where you want to keep state, prefer hiding with CSS. Note t-else/t-elif must immediately follow the t-if element — no other elements between them.

🏋️ Practical Exercise

Build a Wizard component (multi-step form) using only conditional rendering:

  1. State: step (1, 2, or 3), form data for each step.
  2. Use t-if / t-elif / t-else to show only the current step's form fields.
  3. Show a progress bar with 3 segments — use t-att-class with { 'active': step >= n } for each segment.
  4. Show a "Back" button only when step > 1 (use t-if). Show "Next" when step < 3 and "Submit" when step === 3 (use t-if / t-else).
  5. On "Submit", show a success message (t-if="state.submitted") and hide the form (t-if="!state.submitted").

🔥 Challenge Exercise

Build a component that conditionally renders different content with t-if/t-elif/t-else based on state (e.g. loading/empty/loaded). Explain how OWL evaluates conditions and that t-if adds/removes DOM (unlike hiding with CSS).

📋 Summary

  • t-if removes the element from the DOM when false — the element does not exist, is not measured, and any child component is unmounted.
  • t-elif and t-else="" must be immediate next siblings of a t-if or t-elif.
  • Write t-else="" with the empty string — t-else without a value causes a compile error.
  • Use <t t-if="..."> to conditionally render multiple sibling elements without adding a DOM wrapper.
  • t-if removes from DOM; CSS display:none hides but keeps in DOM. Choose based on toggle frequency and whether you need to preserve child state.
  • Changing a component's t-key forces a fresh remount — useful for resetting state when a key prop (like an ID) changes.

Interview Questions

  • How do you conditionally render in OWL?
  • What directives handle conditions?
  • Does t-if hide or remove the element?
  • How is t-if different from display:none?
  • Can you use expressions in t-if?

FAQ

Does t-if work the same as JavaScript's if statement? +

In terms of truthiness evaluation, yes — t-if uses JavaScript's standard truthy/falsy rules. 0, "", null, undefined, false, and NaN are all falsy. Everything else is truthy. Be careful with 0t-if="state.count" is false when count is 0, which may not be what you want. Use explicit comparison: t-if="state.count !== null" or t-if="state.items.length > 0".

Can I put t-if on a component tag? +

Yes — t-if on a component tag controls whether OWL instantiates the component at all. <MyComponent t-if="state.show"/> will mount and run MyComponent's full lifecycle (including onWillStart) only when state.show is truthy. When it becomes falsy, the component is fully unmounted — onWillUnmount fires and the instance is destroyed.

What is the difference between t-if and t-att-class with a boolean? +

t-if controls whether the element exists in the DOM at all. t-att-class="{ hidden: !show }" keeps the element in the DOM but adds/removes a CSS class. The practical difference: with t-if, a child component inside is mounted/unmounted (lifecycle runs). With the CSS approach, the child stays mounted even when visually hidden — useful when you want the component to keep its state but not be visible.

Can I use t-elif without a preceding t-if? +

No — t-elif must follow a t-if or another t-elif as an immediate sibling. Using it without a preceding t-if causes a template compilation error. Similarly, t-else must follow a t-if or t-elif. The chain must be contiguous siblings with no other elements between them.