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"/>.
| Piece | Role |
|---|---|
props.record | the record being edited |
props.name | which 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
- Create a custom field widget.
- Register it in the fields registry.
- Use it in a view with
widget=. - Read the field value in the component.
- 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.
- 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:
| Prop | Type | Description |
|---|---|---|
| value | any | The current field value (Python type mapped to JS) |
| update(newValue) | Function | Call this to update the field value |
| readonly | Boolean | True when the field is not editable |
| record | Object | The full record object — access other field values |
| name | String | The field name |
| type | String | The 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:
/** @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'],
});
<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:
/** @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,
}),
});
<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>
Using the Widget in XML Views
Reference the widget by its registry key in your view 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.
- Widgets receive
props.value(current value) and callprops.update(newVal)to save changes - Always check
props.readonlybefore enabling edit interactions - Register widgets in
registry.category('fields')withsupportedTypes - Use
extractPropsin the registry entry to map XMLoptionsto 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?
Related Topics
FAQ
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 }).
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.
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.

