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 = "";
}
}
📋 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.
🏋️ 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.
Frequently Asked Questions
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).