What OWL Is#
OWL (Odoo Web Library) is the reactive JavaScript framework that powers the Odoo frontend from version 14 onwards. It replaces the legacy Widget system and is conceptually similar to React or Vue:
- Component-based: UI is composed of reusable, self-contained components
- Reactive state: components re-render when state or props change
- Template-driven: uses an XML template syntax (QWeb for JS) co-located with the component
- Service layer: shared functionality (RPC, notifications, dialogs) is accessed via injected services, not globals
OWL is open-source and maintained by Odoo S.A. You can use it independently of Odoo, but in practice it is tightly coupled to the Odoo web client infrastructure.
Component Anatomy#
A minimal OWL component consists of a JavaScript class and an XML template in the same module file.
/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
export class MyCounter extends Component {
static template = "my_module.MyCounter";
setup() {
this.state = useState({ count: 0 });
}
increment() {
this.state.count++;
}
}<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.MyCounter">
<div class="my-counter">
<span t-esc="state.count"/>
<button t-on-click="increment">+</button>
</div>
</t>
</templates>Key points:
/ @odoo-module /tells the Odoo asset bundler this is an ES modulesetup()is the lifecycle hook for initializing state and servicesuseState()creates a reactive proxy - mutations trigger re-rendersstatic templatelinks the class to its XML template by fully-qualified name
Props#
Props are values passed from a parent component to a child. Declare expected props with the static props definition:
export class ProductCard extends Component {
static template = "my_module.ProductCard";
static props = {
productId: Number,
productName: String,
optional: { type: Boolean, optional: true },
};
}In the parent template:
<ProductCard productId="42" productName="'Odoo T-Shirt'" optional="true"/>OWL validates props against the static props definition in development mode and logs warnings for mismatches.
Services#
Services provide shared functionality. Access them in setup() via useService():
import { Component } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class MyForm extends Component {
static template = "my_module.MyForm";
setup() {
this.orm = useService("orm");
this.notification = useService("notification");
}
async save() {
try {
await this.orm.write("sale.order", [this.props.recordId], { state: "sale" });
this.notification.add("Order confirmed", { type: "success" });
} catch (e) {
this.notification.add("Save failed", { type: "danger" });
}
}
}Common services:
orm: database read/write via JSON-RPCnotification: toast messagesdialog: programmatic dialog openingaction: trigger Odoo actions (open form views, run server actions)router: URL navigation
Template Directives#
OWL templates use a subset of QWeb directives:
| Directive | Purpose |
|---|---|
t-esc | Output escaped text |
t-if / t-else | Conditional rendering |
t-foreach / t-as | List rendering |
t-on-click | Event handler binding |
t-model | Two-way input binding |
t-component | Dynamic component name |
t-slot | Slot content injection |
<t t-foreach="state.items" t-as="item" t-key="item.id">
<div t-if="item.active" t-on-click="() => this.select(item.id)">
<span t-esc="item.name"/>
</div>
</t>Always include t-key on t-foreach loops - OWL uses it for efficient DOM diffing.
Registering a Component in the Odoo UI#
To add your component to an existing Odoo view, use the registry:
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { MyWidget } from "./my_widget";
registry.category("fields").add("my_widget", {
component: MyWidget,
supportedTypes: ["char", "many2one"],
});Then reference it in an arch:
<field name="my_field" widget="my_widget"/>Key Differences from the Legacy Widget System#
| Concept | Legacy Widget | OWL |
|---|---|---|
| Base class | Widget (Backbone-based) | Component (OWL) |
| State management | Manual DOM / this.set() | useState() reactive proxy |
| Template | QWeb in XML with widget_name | Same QWeb, named module.ComponentName |
| Services | this._rpc(), global singletons | useService("orm") |
| Lifecycle | init, start, destroy | setup, onMounted, onWillUnmount |
| Module loading | AMD (odoo.define()) | ES modules (/ @odoo-module /) |
Legacy widgets still work in Odoo 16/17 but are deprecated. New development should use OWL components.
Common Gotchas#
1. Missing t-key on loops causes unexpected re-renders. OWL cannot efficiently diff lists without stable keys.
2. Mutating props directly raises an error. Props are read-only; lift state to the parent or emit an event.
3. async setup() is not supported. Do async work in onMounted or in a useEffect pattern with a dedicated loading state.
4. / @odoo-module / comment must be the first line. If it appears after other code or imports, the bundler may not recognize it as an ES module.
5. Service access outside of setup() throws. Store the service reference in setup() and use it from methods.
ERPeek can answer "which OWL components are defined in this module?" and "what services does this component depend on?" across any Odoo 14+ codebase. See the contact page for a demo.

