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.
<!-- 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.
<!-- 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>
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
<!-- 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>:
<!-- 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>
DOM Removal vs CSS Hiding
This is the most important distinction about t-if. It is fundamentally different from display: none:
| Aspect | t-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 cost | Higher — creates/destroys DOM + lifecycle | Lower — just CSS property change |
| Best for | Rarely shown content, expensive children | Frequent show/hide, preserve state |
<!-- ✅ 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
<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:
<!-- 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
<!-- 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 false | CSS display:none | |
|---|---|---|
| In the DOM? | no — removed | yes — hidden |
| Component state | destroyed (setup re-runs on return) | preserved |
| Best when | rarely shown / expensive | toggled 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:
- State:
step(1, 2, or 3), form data for each step. - Use
t-if / t-elif / t-elseto show only the current step's form fields. - Show a progress bar with 3 segments — use
t-att-classwith{ 'active': step >= n }for each segment. - Show a "Back" button only when
step > 1(uset-if). Show "Next" whenstep < 3and "Submit" whenstep === 3(uset-if / t-else). - 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-ifremoves the element from the DOM when false — the element does not exist, is not measured, and any child component is unmounted.t-elifandt-else=""must be immediate next siblings of at-ifort-elif.- Write
t-else=""with the empty string —t-elsewithout a value causes a compile error. - Use
<t t-if="...">to conditionally render multiple sibling elements without adding a DOM wrapper. t-ifremoves from DOM; CSSdisplay:nonehides but keeps in DOM. Choose based on toggle frequency and whether you need to preserve child state.- Changing a component's
t-keyforces 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-ifhide or remove the element? - How is
t-ifdifferent fromdisplay:none? - Can you use expressions in
t-if?
Related Topics
FAQ
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 0 — t-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".
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.
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.
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.

