Advertisement
🦉 OWL in Odoo

Custom Field Widgets in Odoo – OWL Widget Development

Custom field widgets let you render and edit model fields in a completely custom way in Odoo's backend views. Instead of the default text input or dropdown, you can show a color picker, a star rating, a progress bar, or a map widget — all powered by OWL components registered in the fields registry.

⏱️ 25 min 🎯 Advanced 📅 Updated 2026

Custom Field Widgets: Changing How a Field Looks

A widget controls how a single field is rendered and edited in a view — a color picker instead of a text box, a progress bar instead of a number. Odoo ships many (widget="badge", widget="many2many_tags"); when none fit, you build your own as an OWL component and register it.

import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";

class StarRating extends Component {
    static template = "my_module.StarRating";
    static props = { ...standardFieldProps };

    get value() { return this.props.record.data[this.props.name]; }
    setRating(n) {
        this.props.record.update({ [this.props.name]: n });   // write back
    }
}
// register so views can use widget="star_rating"
registry.category("fields").add("star_rating", { component: StarRating });

Once registered, any view uses it declaratively: <field name="score" widget="star_rating"/>.

PieceRole
props.recordthe record being edited
props.namewhich field this widget renders
record.update(...)write the new value back to the form
registry "fields"maps a widget name to your component

Key idea: a widget reads its value from props.record.data[props.name] and writes changes with record.update() so the form's dirty/save machinery keeps working. Spread standardFieldProps so your widget accepts the same props (readonly, required) every field widget gets. Prefer a standard widget when one exists — only build custom for genuinely new interactions.

🏋️ Practical Exercise

  1. Create a custom field widget.
  2. Register it in the fields registry.
  3. Use it in a view with widget=.
  4. Read the field value in the component.
  5. Propagate updates back to the record.

🔥 Challenge Exercise

Build a custom field widget (e.g. a colored badge for a status field), register it in the fields registry, and use it in a form view via widget="...". Explain how field widgets receive the record and propagate changes back through the ORM.

What you'll learn:
  • The widget contract: props.value, props.update(), props.readonly
  • Building a read-only widget (e.g., color swatch)
  • Building an editable widget (e.g., star rating)
  • Registering the widget and using it in a view XML
  • Supporting list view columns

The Widget Contract

A field widget receives standardized props from the view framework. The key props are:

PropTypeDescription
valueanyThe current field value (Python type mapped to JS)
update(newValue)FunctionCall this to update the field value
readonlyBooleanTrue when the field is not editable
recordObjectThe full record object — access other field values
nameStringThe field name
typeStringThe field type (char, integer, selection, etc.)

Building a Read-Only Widget

A color swatch widget that renders a hex color value as a colored circle:

JavaScript
/** @odoo-module **/
import { Component } from '@odoo/owl';
import { registry } from '@web/core/registry';
import { standardFieldProps } from '@web/views/fields/standard_field_props';

export class ColorSwatchField extends Component {
    static template = 'my_module.ColorSwatchField';

    static props = {
        ...standardFieldProps,  // includes value, update, readonly, etc.
    };

    get hexColor() {
        return this.props.value || '#ffffff';
    }
}

registry.category('fields').add('color_swatch', {
    component: ColorSwatchField,
    displayName: 'Color Swatch',
    supportedTypes: ['char'],
});
XML (OWL template)
<templates>
  <t t-name="my_module.ColorSwatchField">
    <div class="o_field_color_swatch">
      <span t-attf-style="background-color: #{hexColor}; display: inline-block;
                          width: 20px; height: 20px; border-radius: 50%;
                          border: 1px solid #ccc;"/>
      <span class="o_color_value" t-esc="hexColor"/>
    </div>
  </t>
</templates>

Building an Editable Widget

A star rating widget for an integer field (0-5 stars). Calls props.update() on click:

JavaScript
/** @odoo-module **/
import { Component } from '@odoo/owl';
import { registry } from '@web/core/registry';
import { standardFieldProps } from '@web/views/fields/standard_field_props';

export class StarRatingField extends Component {
    static template = 'my_module.StarRatingField';
    static props = {
        ...standardFieldProps,
        maxStars: { type: Number, optional: true },
    };
    static defaultProps = { maxStars: 5 };

    get stars() {
        const max = this.props.maxStars;
        const val = this.props.value || 0;
        return Array.from({ length: max }, (_, i) => ({
            filled: i < val,
            index: i + 1,
        }));
    }

    onClick(star) {
        if (!this.props.readonly) {
            this.props.update(star.index);
        }
    }
}

registry.category('fields').add('star_rating', {
    component: StarRatingField,
    displayName: 'Star Rating',
    supportedTypes: ['integer'],
    extractProps: ({ options }) => ({
        maxStars: options.max_stars || 5,
    }),
});
XML (OWL template)
<t t-name="my_module.StarRatingField">
  <div class="o_field_star_rating">
    <t t-foreach="stars" t-as="star">
      <span t-attf-class="fa #{star.filled ? 'fa-star' : 'fa-star-o'}
                           #{props.readonly ? '' : 'o_clickable'}"
            t-on-click="() => onClick(star)"/>
    </t>
  </div>
</t>
Advertisement

Using the Widget in XML Views

Reference the widget by its registry key in your view XML:

XML
<!-- Form view -->
<field name="rating" widget="star_rating" options="{'max_stars': 5}"/>
<field name="color" widget="color_swatch"/>

<!-- List view column -->
<field name="rating" widget="star_rating" optional="show"/>

The options attribute passes a JSON object to extractProps() in the registry definition. Use it to configure widget behavior per field usage.

Supporting List View Columns

For list view support, the widget must handle readonly=true gracefully (list view is always readonly). The same component works in both form and list views — just ensure the template renders a compact representation when props.readonly is true.

Key takeaways:
  • Widgets receive props.value (current value) and call props.update(newVal) to save changes
  • Always check props.readonly before enabling edit interactions
  • Register widgets in registry.category('fields') with supportedTypes
  • Use extractProps in the registry entry to map XML options to component props

Interview Questions

  • What is a custom field widget?
  • How do you register a widget?
  • How do you use a widget in a view?
  • How does a widget read and write the field value?
  • When would you build a custom widget instead of using a built-in one?

FAQ

How do I make a widget that spans multiple fields? +

Use props.record to access other field values: this.props.record.data.other_field. Your widget is still bound to one field (for the props.update() contract), but it can read any field from the record. To update multiple fields at once, use props.record.update({ field1: val1, field2: val2 }).

Can I use an existing widget as a base and extend it? +

Yes — import the existing widget class and extend it: class MyCharField extends CharField. Override setup() or get displayValue() to modify behavior. Re-register under a new key in the registry. Don't re-register under the existing key unless you want to replace the widget for ALL usages of that type.

How do I handle async data loading in a widget? +

Use useState() for loading state and onMounted() to fetch data after the widget mounts. For data that depends on props.value, also use onWillUpdateProps() to refetch when the value changes. Always show a loading indicator while async data is being fetched to avoid blank renders.