Ad – 728×90
🦉 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
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>
Ad – 728×90

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

Frequently Asked Questions

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.