If you already work with OWL components, list renderers, or custom client actions, useSortable is one of those frontend utilities that can make an Odoo screen feel much more natural. It is especially useful when users need drag and drop sorting instead of clicking arrows, editing sequence values manually, or opening forms just to change order. This kind of UI pattern often appears in task boards, checklist builders, dashboard blocks, configurable steps, and internal tools.
From a business angle, this is also where technical UX starts to matter. If a team wants a smoother planning screen or a cleaner ranking workflow, the implementation usually sits inside custom OWL work, renderer extensions, or client actions. That is often the kind of requirement that comes up during an Odoo ERP Consultation in Australia and USA, especially when a business wants custom behavior without breaking the core Odoo flow. Odoo 19’s web client is an Owl-based single-page application, and Odoo’s own developer documentation explicitly says new development should be done in Owl when possible.
Why useSortable Matters in Real Odoo Interfaces
At a practical level, useSortable gives you a hook-based way to manage draggable, reorderable elements inside the Odoo web client. In Odoo 19, the OWL-ready version exported from @web/core/utils/sortable_owl wraps the native sortable utility and wires it into Owl lifecycle and reactivity tools such as useEffect, onWillUnmount, useExternalListener, throttling for animation, and reactive state wrapping. That is what makes it fit naturally inside modern Odoo frontend components instead of feeling like an external drag library bolted on afterward.
Common Business and Developer Use Cases for Drag and Drop
For developers, the most common use case is reordering records that already have a sequence field. For business owners, the visible benefit is simpler prioritization. Think of sales stages inside a custom lane, approval steps in a checklist, a ranking list of products for internal planning, or dashboard widgets users want to rearrange. The frontend interaction is drag and drop, but the real goal is faster decision-making with less friction.
Where useSortable Fits in the Odoo 19 Frontend Stack
Odoo’s frontend architecture matters here. The framework overview explains that the web client is an Owl application, imports under web/static/src are available with the @web prefix, and built-in views are structured around components such as controllers and renderers. That means useSortable is not a standalone trick. It belongs in the same frontend ecosystem as services, hooks, registries, and renderer customizations.
OWL Components, Hooks, and Reactive UI Logic
Odoo’s documentation describes Owl as the component framework behind the modern client, with QWeb templates enriched by Owl directives. Hooks are the standard way to factor logic that depends on component lifecycle. In other words, useSortable belongs in setup() next to the rest of your hook-based logic, not in old-style jQuery patches or legacy widget code.
How useSortable Relates to @web/core/utils/sortable
This is the clean mental model: @web/core/utils/sortable contains the native sortable hook logic, while @web/core/utils/sortable_owl provides the Owl-friendly wrapper. In Odoo 19, that wrapper passes lifecycle setup, teardown, listener registration, throttling, and reactivity helpers into the native hook. So for normal OWL component work, sortable_owl is the import most developers want.
Need help applying this to your business?
Core Concepts You Need Before Writing Code
Before touching code, keep four things straight: your sortable container, your sortable items, your stable item identity, and your persistence logic. The hook needs a container ref and a selector that identifies which children are sortable. Your OWL template also needs stable keys so re-rendering does not confuse the DOM order. Odoo’s Owl tutorial explicitly notes that t-foreach requires a unique t-key so Owl can reconcile each element correctly.
Containers, Sortable Items, and Stable Record Identifiers
The official sortable source marks ref and elements as mandatory parameters. The hook then works against the referenced DOM root and the matching element selector. In practice, each record row should also carry a stable ID such as data-id, because your onDrop handler usually needs to know which record moved and what its new neighbors are. The onDrop payload can include element, group, previous, next, and parent, which is exactly what you need to compute a new sequence.
Refs, DOM Targets, and Lifecycle-Aware Hook Setup
Odoo’s docs show the standard pattern for DOM-aware hooks: define a t-ref in XML and access it with useRef() in setup(). That same pattern works perfectly for useSortable, because the hook needs a real container element to bind listeners and manage placeholder movement.
Building a Basic useSortable Example Step by Step
Here is a clean example pattern for a custom reorderable list. This example is illustrative, but it follows Odoo 19’s official frontend patterns for Owl components, services, refs, and sortable parameters.
Preparing the JavaScript Component
/** @odoo-module **/
import { Component, useRef, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { useSortable } from "@web/core/utils/sortable_owl";
export class SortableStageList extends Component {
static template = "my_module.SortableStageList";
setup() {
this.orm = useService("orm");
this.rootRef = useRef("sortableRoot");
this.state = useState({
items: [
{ id: 10, name: "Qualified", sequence: 1 },
{ id: 11, name: "Proposal Sent", sequence: 2 },
{ id: 12, name: "Negotiation", sequence: 3 },
],
});
useSortable({
ref: this.rootRef,
elements: ".o_sortable_item",
handle: ".o_drag_handle",
enable: () => this.state.items.length > 1,
onDrop: ({ element, previous, next }) => {
const movedId = Number(element.dataset.id);
const previousId = previous ? Number(previous.dataset.id) : null;
const nextId = next ? Number(next.dataset.id) : null;
this.reorderLocally(movedId, previousId, nextId);
this.saveOrder();
},
});
}
reorderLocally(movedId, previousId, nextId) {
const items = [...this.state.items];
const movedIndex = items.findIndex((item) => item.id === movedId);
const [movedItem] = items.splice(movedIndex, 1);
let targetIndex = 0;
if (nextId) {
targetIndex = items.findIndex((item) => item.id === nextId);
} else if (previousId) {
targetIndex = items.findIndex((item) => item.id === previousId) + 1;
}
items.splice(targetIndex, 0, movedItem);
items.forEach((item, index) => {
item.sequence = index + 1;
});
this.state.items.splice(0, this.state.items.length, ...items);
}
async saveOrder() {
for (const item of this.state.items) {
await this.orm.write("crm.stage", [item.id], {
sequence: item.sequence,
});
}
}
}
Structuring the XML Template for Sortable Items
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="my_module.SortableStageList">
<div t-ref="sortableRoot" class="o_stage_sortable_list">
<t t-foreach="state.items" t-as="item" t-key="item.id">
<div class="o_sortable_item d-flex align-items-center gap-2"
t-att-data-id="item.id">
<span class="o_drag_handle fa fa-bars"/>
<span t-esc="item.name"/>
</div>
</t>
</div>
</t>
</templates> Handling Reorder Events Cleanly
The important part is not the drag itself. It is what you do after drop. In Odoo’s native sortable implementation, onDrop is called when the drag ends on pointer-up and the element has actually moved. The returned context includes surrounding elements, which lets you determine the new location with minimal guesswork. That is the right place to update reactive state and then persist a fresh sequence to the server.
Important Configuration Options and What They Change
The official sortable source exposes more than just a basic reorder event. In addition to ref and elements, it supports options such as enable, delay, touchDelay, groups, handle, ignore, connectGroups, cursor, clone, placeholderClasses, applyChangeOnDrop, followingElementClasses, and callbacks like onDragStart, onElementEnter, onGroupEnter, onDragEnd, and onDrop.
Drag Handle, Enable State, Groups, Delay, and Tolerance
A few of these are especially useful in real projects. handle is excellent when you want dragging only from a small icon, not from the entire row. enable is useful when reorder should be disabled in readonly mode or for users without permission. groups and connectGroups are what you use when items need to move across containers instead of only inside one list. delay and touchDelay matter on touch-heavy screens where accidental drags are a pain.
Persisting the New Order in Odoo
Once the UI order changes, the backend usually needs to reflect that change through a sequence-style field. Odoo’s JavaScript reference explains that model method calls should go through the orm service and that components should access services through useService. That is why persisting sortable changes with this.orm.write(...) or this.orm.call(...) is the normal Odoo way to do it.
Updating Sequence Values Safely
The safest pattern is to rebuild the full order locally, assign fresh sequence values in memory, and then write them back. That keeps frontend and backend aligned. If you only patch one row without recomputing surrounding rows, gaps and collisions can creep in later.
Calling ORM or RPC After a Drop Action
Use the orm service when you are calling model methods. Use rpc only when you really need a controller route. Odoo’s docs are pretty explicit about this distinction, and following it keeps your custom module closer to standard framework practice.
Best Practices for Performance, UX, and Maintainability
A good useSortable implementation should feel boring in production. No jumping rows, no duplicated records, no “it looked reordered until refresh” moments. That usually comes down to three things: stable t-key values, state updates that mirror the new order, and persistence that happens right after a successful drop.
Midway through a frontend customization project, it is often worth revisiting the bigger view architecture too. If you are also extending controllers, renderers, or js_class behavior, this related guide on Extending Odoo 19 Views with Custom JS.
Avoiding DOM Conflicts and Unstable Keys
This is the classic trap. If Owl thinks one row is another row because your t-key is weak or missing, your drag result may look inconsistent after re-render. Odoo’s tutorial warning on unique t-key values is very relevant here. Sortable behavior can only feel stable if Owl reconciliation is stable too.
Designing for Large Lists and Real Business Data
For larger lists, keep the UI lean. Use a handle, avoid extra heavy DOM inside each row, and only write what you need after drop. Need help implementing that inside a real custom module or view extension? Book a consultation.
Common Mistakes Developers Make with useSortable
The biggest mistakes are predictable: importing the wrong module, skipping t-ref, forgetting unique t-key, relying only on DOM movement without updating Owl state, and not persisting the new order. Another easy mistake is misunderstanding applyChangeOnDrop. In the native sortable implementation, that flag controls whether the DOM change is applied automatically on drop. Even when you use it, you should still update your reactive state so Owl remains the source of truth.
When Business Owners Should Ask for This Pattern in Custom Modules
If your team keeps saying things like “can we just drag these into order?” then this pattern is probably a good fit. It works best when the business process is naturally order-driven and the new order needs to be saved, not just displayed. Good examples include pipeline priorities, internal task sequences, dashboard blocks, onboarding steps, and configurable workflows.
Final Thoughts
useSortable in Odoo 19 is not just about drag and drop eye candy. It is a practical OWL hook pattern that connects DOM reordering, component lifecycle, reactive state, and server persistence in a way that matches how the Odoo web framework is designed. Once you understand the container ref, sortable selector, stable keys, and post-drop persistence flow, the hook becomes straightforward and very reusable.
You’re here because something matters.
If this decision impacts your operations, your team, or your growth
Let’s talk before it becomes harder to undo.
Frequently Asked Questions (FAQs)
1. Do I import from sortable or sortable_owl?
For most Odoo 19 OWL components, use @web/core/utils/sortable_owl. That wrapper injects Owl-aware lifecycle and reactivity helpers into the native sortable hook.
2. Do I need a sequence field in the model?
Usually yes, if the new order must survive page reloads or be shared with other users. The frontend can reorder visually, but persistent business order normally belongs in a backend field.
3. Can useSortable work across multiple containers?
Yes. The official parameters include groups and connectGroups, and there are dedicated group enter and leave callbacks for that behavior.
4. What happens if I forget t-key in my loop?
You risk unstable rendering because Owl requires a unique t-key for t-foreach reconciliation. In sortable UIs, that problem becomes visible very quickly.
5. Should I use orm or rpc to save the new order?
Use orm when you are calling model methods or writing model fields. Use rpc only when you really need a controller route. That is the distinction Odoo’s JavaScript reference gives for server communication.
Real Stories. Real Results.
See what our clients have to say — in their own words. These video testimonials share genuine experiences from business owners and teams who’ve transformed their operations with Odoo. From smoother workflows to faster decision-making, their stories reflect the real impact of getting the right system and guidance.