Setting Up a Ref
Two steps: call useRef("name") in the component and add t-ref="name" to the element in the template. The string name connects them.
import { Component, xml, useRef, onMounted } from "@odoo/owl";
class AutoFocus extends Component {
static template = xml`
<div>
<!-- Step 2: mark the element with t-ref -->
<input t-ref="searchInput" type="text" placeholder="Search…"/>
<button t-on-click="focusSearch">Focus Search</button>
</div>
`;
// Step 1: create the ref — name must match t-ref value
searchInput = useRef("searchInput");
setup() {
onMounted(() => {
// .el is the real DOM element — available after mount
this.searchInput.el?.focus();
});
}
focusSearch() {
this.searchInput.el?.focus();
}
}
myRef.el is null before the component mounts and after it unmounts. Using this.myRef.el?.focus() (optional chaining) safely no-ops when the element is not yet available, instead of throwing Cannot read properties of null. Make this a habit.
Ref Availability Timing
| Lifecycle Point | ref.el value |
|---|---|
During setup() | null — not yet rendered |
During onWillStart() | null — not yet rendered |
During onMounted() | ✅ Real DOM element |
| In event handlers (after mount) | ✅ Real DOM element |
During onWillPatch() / onPatched() | ✅ Real DOM element |
During onWillUnmount() | ✅ Real DOM element (last chance) |
| After component destroyed | null |
Multiple Refs
Create as many refs as you need — one useRef() call per element:
class RichEditor extends Component {
static template = xml`
<div class="editor">
<div t-ref="toolbar" class="toolbar">
<button t-on-click="bold">B</button>
<button t-on-click="italic">I</button>
</div>
<div t-ref="editable" class="content" contenteditable="true"></div>
<div t-ref="statusBar" class="status-bar"></div>
</div>
`;
toolbar = useRef("toolbar");
editable = useRef("editable");
statusBar = useRef("statusBar");
setup() {
onMounted(() => {
this.editable.el?.focus();
// Measure toolbar height to set editor padding
const toolbarH = this.toolbar.el?.getBoundingClientRect().height || 0;
if (this.editable.el) {
this.editable.el.style.paddingTop = toolbarH + "px";
}
});
}
bold() { document.execCommand("bold"); }
italic() { document.execCommand("italic"); }
}
Refs on Conditional Elements
If the element with t-ref is inside a t-if, the ref is only set when the element is in the DOM. When the condition becomes false and the element is removed, ref.el returns to null.
class SearchPanel extends Component {
state = useState({ isOpen: false });
inputRef = useRef("searchInput");
static template = xml`
<div>
<button t-on-click="openPanel">Open Search</button>
<!-- Ref only valid when panel is open -->
<div t-if="state.isOpen" class="search-panel">
<input t-ref="searchInput" type="search"/>
</div>
</div>
`;
openPanel() {
this.state.isOpen = true;
// Can't focus here — DOM doesn't exist yet (state mutation is async micro-task)
// Use onPatched instead:
}
setup() {
let _wasOpen = false;
onPatched(() => {
if (this.state.isOpen && !_wasOpen) {
// Panel just became visible — focus the input
this.inputRef.el?.focus();
}
_wasOpen = this.state.isOpen;
});
}
}
When you set state to show an element and immediately try to focus its ref, the DOM has not updated yet — the focus call runs before OWL patches the DOM. Use onPatched to detect when the element just appeared and focus it there. Alternatively, use await Promise.resolve() to defer to the next microtask after OWL's render — but onPatched is the idiomatic OWL approach.
Dynamic Ref Names
Ref names can be dynamic expressions in t-ref — useful in loops:
class TabPanel extends Component {
static template = xml`
<div>
<!-- Dynamic ref name per tab -->
<div
t-foreach="props.tabs"
t-as="tab"
t-key="tab.id"
t-att-ref="'tab-' + tab.id"
class="tab-panel"
t-att-class="{ active: state.activeId === tab.id }"
>
<t t-esc="tab.content"/>
</div>
</div>
`;
state = useState({ activeId: null });
// Access a specific tab's DOM element
scrollToTab(id) {
// Dynamic refs accessed via this.refs (not useRef)
const el = this.refs["tab-" + id];
el?.scrollIntoView({ behavior: "smooth" });
}
}
When t-ref uses a dynamic expression (t-att-ref), the element is stored in this.refs["refName"] — an object of all current refs on the component. Static refs created with useRef("name") are also accessible in this.refs.name, but the idiomatic way is through the ref object returned by useRef().
Common Ref Patterns
// 1. Measure element dimensions after render
class AdaptiveLayout extends Component {
containerRef = useRef("container");
state = useState({ columns: 1 });
setup() {
onMounted(() => this.updateColumns());
onPatched(() => this.updateColumns());
}
updateColumns() {
const width = this.containerRef.el?.getBoundingClientRect().width || 0;
this.state.columns = width > 900 ? 3 : width > 600 ? 2 : 1;
}
}
// 2. Programmatic scroll
class ChatList extends Component {
listRef = useRef("list");
scrollToBottom() {
const el = this.listRef.el;
if (el) el.scrollTop = el.scrollHeight;
}
scrollToItem(id) {
const item = this.listRef.el?.querySelector(`[data-id="${id}"]`);
item?.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
}
// 3. Read input value imperatively (as alternative to t-model)
class FileUpload extends Component {
fileInput = useRef("fileInput");
getSelectedFiles() {
return Array.from(this.fileInput.el?.files || []);
}
clearInput() {
if (this.fileInput.el) this.fileInput.el.value = "";
}
}
Refs: Reaching a Real DOM Element When You Must
Most of the time you let OWL update the DOM. But sometimes you need the actual element — to focus an input, measure size, or hand it to a chart library. That's what t-ref + the useRef hook are for.
import { Component, useRef, onMounted, xml } from "@odoo/owl";
class SearchBox extends Component {
static template = xml`<input t-ref="search" placeholder="Search…"/>`;
setup() {
this.searchInput = useRef("search"); // name matches t-ref
onMounted(() => {
this.searchInput.el.focus(); // .el = the real DOM node
});
}
}
The timing rule: ref.el is null until the element is actually in the DOM. So you access it in onMounted (or later), never in setup() or onWillStart — the DOM doesn't exist yet there. Reading .el too early is the most common ref bug.
| Do with a ref | Don't |
|---|---|
| focus, scroll, measure, media playback | read/write data (use state) |
| integrate a 3rd-party DOM library | manually change what OWL renders |
Refs are an escape hatch for imperative DOM work. For anything that affects what's displayed, change state and let OWL re-render — fighting the framework by mutating DOM directly leads to inconsistencies.
🏋️ Practical Exercise
Build a ContentEditableEditor component using refs for direct DOM control:
- Render a
<div contenteditable="true" t-ref="editor">. - In
onMounted, focus the editor. - Add a "Bold" button that calls
document.execCommand("bold"). Refocus the editor after the command. - Add a "Clear" button that sets
this.editorRef.el.innerHTML = ""and refocuses. - Add a character counter: listen to the
inputevent on the editor div (uset-on-input), readevent.target.innerText.length, and updatestate.charCount. - Add a "Get Content" button that reads
this.editorRef.el?.innerHTMLand logs it. Show the raw HTML below the editor in a<pre>tag usingt-esc.
🔥 Challenge Exercise
Use useRef + t-ref to grab a DOM element and focus it when the component mounts. Explain when to reach for a ref (direct DOM access) versus reactive state, and why ref access should happen after mount.
📋 Summary
- Two steps:
myRef = useRef("name")in the class +t-ref="name"on the element in the template. - Access the DOM element via
this.myRef.el. Always use optional chaining (?.el) — it isnullbefore mount and after unmount. - Ref is available from
onMountedthroughonWillUnmount. Before that:null. After:null. - For conditional elements (
t-if), the ref isnullwhen the element is not in the DOM. - To focus an element that just appeared via
t-if, useonPatched— not an immediate focus after setting state. - Dynamic ref names (from loops) use
t-att-refand are accessed viathis.refs["name"]. - Use refs only when you need direct DOM access — for everything else (text, classes, attributes), use the reactive template system.
Interview Questions
- What is a ref in OWL?
- How do you create and attach a ref?
- When is a ref available (which lifecycle)?
- When should you use a ref instead of state?
- How do you focus an input with a ref?
Related Topics
FAQ
Yes — t-ref on a component tag gives you the component instance (not a DOM element). this.myRef.comp is the child component instance; this.myRef.el is its root DOM element. However, accessing child component internals from a parent is an anti-pattern — it breaks encapsulation. Prefer props and callback props for communication. Use component refs only for legitimate imperative use cases like calling a child's scrollToTop() method.
If you use t-ref on an element inside a t-if, the element is only in the DOM when the condition is true. If the condition is false when the handler runs, ref.el is null. Check the condition first: if (!this.myRef.el) return;. Also verify the ref name in useRef("name") exactly matches the t-ref="name" string — they are case-sensitive.
They serve different purposes. useState holds reactive data that the template renders — mutations trigger re-renders. useRef holds a mutable reference that does not trigger re-renders — OWL sets its .el property when the DOM element appears or disappears. Use useState for data that affects what the template shows. Use useRef when you need to imperatively access a DOM element. Storing a DOM element in useState would cause unnecessary re-renders every time the element is set; that is why they are separate.
Yes — this.el.querySelector(".my-input") is an alternative to useRef when you do not want to add a t-ref attribute. However, useRef is preferred because it is more explicit (names the intention), more stable (does not depend on CSS class names that might change), and more efficient (OWL fills it directly rather than requiring a DOM traversal). Reserve querySelector for cases where you cannot modify the template (e.g., content inserted by a third-party library).

