Ad – 728×90
🦉 Dynamic Templates

OWL JS Refs – t-ref and useRef for Direct DOM Access

OWL's declarative data flow handles 95% of UI work — but sometimes you need a direct reference to a real DOM element: to call .focus(), measure dimensions with getBoundingClientRect(), initialize a third-party library, or imperatively scroll to a position. OWL provides two pieces for this: t-ref marks an element in the template, and useRef() creates the ref object that OWL fills with the real DOM element after mounting. This lesson covers everything about refs: timing, multiple refs, conditional refs, and every real-world use case.

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

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.

JavaScript – useRef + t-ref
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();
  }
}
ℹ️
Always use optional chaining: .el?.

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 Pointref.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 destroyednull

Multiple Refs

Create as many refs as you need — one useRef() call per element:

JavaScript – multiple refs
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"); }
}
Ad – 336×280

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.

XML + JavaScript – conditional ref
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;
    });
  }
}
💡
Focus after conditional render: use onPatched

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:

XML + JavaScript – dynamic ref in a loop
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" });
  }
}
ℹ️
Dynamic refs use this.refs, not useRef()

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

JavaScript – real-world 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 is null before mount and after unmount.
  • Ref is available from onMounted through onWillUnmount. Before that: null. After: null.
  • For conditional elements (t-if), the ref is null when the element is not in the DOM.
  • To focus an element that just appeared via t-if, use onPatched — not an immediate focus after setting state.
  • Dynamic ref names (from loops) use t-att-ref and are accessed via this.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:

  1. Render a <div contenteditable="true" t-ref="editor">.
  2. In onMounted, focus the editor.
  3. Add a "Bold" button that calls document.execCommand("bold"). Refocus the editor after the command.
  4. Add a "Clear" button that sets this.editorRef.el.innerHTML = "" and refocuses.
  5. Add a character counter: listen to the input event on the editor div (use t-on-input), read event.target.innerText.length, and update state.charCount.
  6. Add a "Get Content" button that reads this.editorRef.el?.innerHTML and logs it. Show the raw HTML below the editor in a <pre> tag using t-esc.

Frequently Asked Questions

Can I put t-ref on a component tag to get the component instance? +

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.

Why is my ref null when I try to use it in an event handler? +

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.

Is useRef the same as useState? When should I use each? +

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.

Can I use querySelector on this.el instead of useRef? +

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