Skip to main content
iVentureTeam

radio_followed_by_element

New in Odoo 19: radio_followed_by_element physically moves other form elements so they sit inline after a chosen radio option. It powers the accrual plan sentences in Time Off, and it is the most DOM-invasive field widget in core.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 21, 2026Updated August 21, 20266 min read
Technical nameradio_followed_by_element
Field typesselection, many2one
Viewsform
Modulehr
Used in core3 occurrences across 1 module: accrual plan level forms in hr_holidays
VersionsOdoo 20.0, Odoo 19.0
No-code setupNo. Applied via the widget attribute in view XML
Alternativesradio, selection, selection_badge, float_without_trailing_zeros

What the Radio with inline element does

Some configuration reads best as a sentence with a blank in it: "carry over on ( ) January 1st ( ) other: [date]". HTML forms fight this layout, because the date input belongs to the form's field grid while the radio options are rendered inside a single field widget. Odoo 19's answer in the Time Off app is this widget: a radio field that, after mounting, finds designated elements elsewhere in the view by id and appends each one directly after the radio option it belongs to.

Configuration is two options. links is a mapping from radio option values to DOM element ids, for example {'other': 'carryover_custom_date'}: the element with id carryover_custom_date is moved to sit after the option whose value is other. observe names the container (matched by the name attribute) that the widget watches with a MutationObserver, so when Odoo re-renders that area and recreates the elements in their original spots, the widget moves them back into place.

Everything about the radio itself, value handling, rendering of options, is inherited from the standard radio widget.

What this means for your team

The payoff is forms non-specialists can read aloud. Odoo's accrual plan editor asks questions like when a milestone triggers or how carryover caps apply, and each answer may need a companion value: a date, a number of days. Rendering those companions inline after the option that activates them, instead of in a detached row below, is the difference between a policy screen an HR manager configures alone and one that needs a consultant on a call.

For customizers, this widget is interesting as a sanctioned pattern. "Put this input inside that widget's layout" is a request every Odoo integrator has heard, and the usual answers, template inheritance or CSS contortions, are brittle. Core now demonstrates the DOM-move approach with an observer to keep it stable across re-renders. It is still the most invasive trick in the fields registry, and the constraints are real: ids must be unique and present, and anything else that moves those nodes will fight the observer. Use it for sentence-style config screens, not as a general layout tool.

Supported options in Odoo 19

Verified against radio_followed_by_element.js in the Odoo 19.0 hr module. Both options are declared in supportedOptions and, unusually, both map to required component props.

OptionTypeWhat it does
linksobjectRequired. Maps radio option values to DOM element ids: each element is moved to sit directly after its option. Keys must be usable inside a CSS attribute selector.(since Odoo 19.0)
observestringRequired. The name attribute of the container watched by a MutationObserver; every re-render inside it re-triggers the element moves. Must exist when the widget mounts.(since Odoo 19.0)

There are no optional knobs here. Omitting links or observe is not a degraded mode, it is a props validation error that breaks the view. Note also that this widget's extractProps replaces the base radio's entirely, so the inherited horizontal option is accepted by the schema but never reaches the component: these radios are always vertical.

Working examples

As core uses it for the carryover date

<field class="ms-1" name="carryover_date"
       widget="radio_followed_by_element"
       options="{'links': {'other': 'carryover_custom_date'}, 'observe': 'carryover'}"/>
<span id="carryover_custom_date">
    <!-- the inline date fields live here -->
</span>

The span with the matching id is authored anywhere convenient in the same view; the widget relocates it after the other option. The observed container is the element whose name attribute is carryover.

Several options with companions

<field name="milestone_date" widget="radio_followed_by_element"
       options="{'links': {'after': 'milestone_date_after', 'on': 'milestone_date_on'}, 'observe': 'milestone_date'}"/>

Each entry in links pairs one radio value with one element id, so different options can each get their own inline companion.

How the DOM surgery actually works

Reading moveElement closely tells you where this can bite.

Matching is by data-value attribute selector. The radio options render with data-value set to the selection value, and the widget queries [data-value=<key>] globally on the document, taking the first match. Two consequences: keys must be valid unquoted CSS attribute selector material, which numeric-looking or exotic values are not, and two instances of this widget with overlapping selection values on one page can grab each other's options.

The move is appendChild into the option's parent. Not a copy: the element genuinely leaves its authored position, with its Owl bindings intact, which is why the moved element can contain live fields. The guard skips the move when the element is already in place.

The observer's filter is broken and it does not matter. The MutationObserver callback builds a list of added node ids and calls .filter(...) on it, then treats the resulting array as a condition. An array is always truthy in JavaScript, so the widget re-runs the move on every childList mutation in the observed subtree, not just relevant ones. The intended optimization never fires; the observed effect, elements snapping back after each re-render, works anyway because the move is idempotent. The same code survives on master with cosmetic reformatting, so this stays true for Odoo 20 as of today.

The observed element is found by document.getElementsByName. Meaning the observe value must match a name attribute, the convention Odoo form groups and spans already follow, and it must exist at mount or the observer call throws.

Version compatibility

VersionStatusNotes
Odoo 20.0In developmentNot released. Formatting-only changes on the development branch; behavior identical.
Odoo 19.0VerifiedWidget introduced this release; verified against the shipped source.
Odoo 18.0Not availableWidget does not exist.
Odoo 17.0Not availableWidget does not exist.
Odoo 16.0Not availableWidget does not exist.

Upgrade note. The widget does not exist before Odoo 19. Views backported to 18 must fall back to conventional layouts, or carry the widget file along as a custom module, which works since it only depends on the standard radio field.

What is changing in Odoo 20

Odoo 20 is expected at Odoo Experience in Brussels, 24 to 26 September 2026. These notes are read from the public development branch, which is unstable until feature freeze; we re-verify against the shipped release.

The file survives on master with formatting-only changes: the code is reindented and the props migrate to the new validation syntax, with links and observe still required. The registration, both options, the global data-value querying, and the always-truthy observer filter are all unchanged. No behavioral difference is expected for Odoo 20 based on the branch today.

Common problems and fixes

SymptomCause and fix
View crashes with a props validation errorlinks and observe are required props; one is missing from options. Provide both options on every use of the widget.
The companion element renders in its original spotThe element id in links does not match, or the option's data-value key is wrong. Check ids and selection values character for character; matching is exact.
Error mentioning getElementsByName or observe on mountNo element with the observed name attribute exists when the widget mounts. Ensure the container named in observe is present and not conditionally hidden at load.
Elements jump back after an onchange, then snap into placeOdoo re-rendered the observed container; the observer re-runs the move just after. No fix needed; a brief flicker on heavy re-renders is inherent to the approach.
Radios will not render horizontally despite the horizontal optionThis widget's extractProps replaces the base radio's and never passes orientation. None without a custom widget; the layout is vertical by design.
Two widgets on one form move each other's elementsdata-value matching queries the whole document and takes the first match. Keep selection values distinct between co-rendered instances.

Radio with inline element vs the alternatives

WidgetBest forKey difference
radio_followed_by_elementSentence-style choices with inline companion inputsPhysically relocates DOM elements after their radio option
radioPlain visible-choice selectionsNo DOM movement; supports the horizontal option this variant drops
selectionCompact dropdown choicesOne closed control, no inline companions
selection_badgeOne-click choice pillsBadges instead of radios, no companion mechanism
float_without_trailing_zerosThe numbers inside those same accrual sentencesFormats a value rather than arranging the layout

If the companion input can live on its own line, the plain radio widget plus conditional visibility is simpler and far less fragile. Reach for this widget only when the inline sentence layout is a hard requirement.

Frequently asked questions

What problem does radio_followed_by_element solve?+
Inline sentence layouts: a radio choice where selecting an option like Other should reveal an input right there, after the option text, rather than somewhere else on the form. The widget moves designated elements by id into position and keeps them there across re-renders.
Are the links and observe options really mandatory?+
Yes. Both are declared as required props, so leaving either out fails props validation and breaks the view. This is unusual; most widget options degrade gracefully.
Can the moved element contain live fields?+
Yes, and in core it does: the relocated spans hold real date and number fields. The move is a DOM appendChild of the original nodes, so Owl's bindings continue to work.
Why do my radios ignore the horizontal option with this widget?+
The variant's extractProps returns only readonly, links, and observe, discarding the base radio's orientation, label, and domain handling. Vertical is the only layout it produces.
Is this widget available in Odoo 17 or 18?+
No. It first ships in Odoo 19 as part of the hr module. Since it only depends on the standard radio widget, backporting it as a small custom module is feasible where the layout is worth it.
How stable is this pattern for custom development?+
It is core-sanctioned but inherently DOM-fragile: exact ids, unique selection values, and a present observed container are all hard requirements, and the observer re-runs moves on every mutation. Use it for config screens, and prefer conditional visibility for ordinary show-hide cases.

Forms your team reads like sentences?

Inline layouts, dependent inputs, and guided configuration screens take Odoo beyond stock view XML. We design and build these UX-heavy forms, custom widgets included, so complex policies become screens people fill in correctly the first time. Odoo 16 through 19.

Book a free consultation

How this page was produced

This page was verified by reading radio_followed_by_element.js on the Odoo 19.0 branch, including the moveElement and observer logic described above, plus the base radio_field.js for what is inherited and what extractProps drops. Absence in 16 through 18 was confirmed against those branches' file trees, and the master diff was reviewed for the Odoo 20 section. Usage was confirmed in the hr_holidays accrual plan views on a clean Odoo 19 database, where the screenshot was captured. Corrections welcome via our contact page.