...

How to Use the useSortable Hook in Odoo 19

For businesses looking to improve frontend workflows and user experience, partnering with an odoo customization company in USA can make Odoo 19 custom development faster, cleaner, and easier to maintain.

If you are building custom frontend behavior in Odoo 19, the useSortable hook is one of the cleanest ways to add drag and drop interaction inside Owl components. For teams working with custom views, embedded widgets, or reorderable action lists, it gives you practical control over sortable UI behavior without falling back to messy manual DOM code. If you are looking for experienced odoo erp consultants in USA to help with frontend customization, this is exactly the kind of technical work that benefits from a solid Odoo web client approach.

At a high level, Odoo’s frontend is built on Owl components, and those components are defined with QWeb templates. That matters because useSortable fits naturally into the same hook-based pattern you already use in setup(), useRef, and service-driven frontend event handling.

Understanding useSortable in Odoo 19

useSortable is used to attach drag and drop behavior to a sortable container so users can reorder draggable elements inside a custom Owl component. In practice, developers commonly import it from @web/core/utils/sortable_owl, initialize it inside setup(), and wire it to callbacks like onWillStartDrag and onDrop to react to user actions.

What makes it useful is not just the drag interaction itself. It also gives you a structured way to control list reordering, highlight the element being dragged, and decide how the new order should be stored in UI state or written back to the database. That makes it a strong fit for custom dashboards, embedded actions, Kanban sorting helpers, or any reorderable block inside the Odoo web client.

How It Fits into OWL and the Odoo Web Client

Odoo 19’s frontend framework treats hooks as reusable lifecycle-aware building blocks. Owl components provide the component model, QWeb template structure, and rendering layer, while hooks let you inject specific behavior into a component without bloating its class logic. useSortable sits neatly in that pattern.

A practical way to think about it is this: Owl handles rendering, QWeb defines the markup, and useSortable manages the drag behavior on top of that rendered DOM. So instead of manually binding and cleaning up listeners across patches and unmounts, you keep the logic inside the component lifecycle where it belongs.

Core Prerequisites Before Implementation

Before you add useSortable, make sure your component already has three basics in place: an Owl component, a QWeb template, and a stable list of items with unique identifiers. Since the hook works by tracking element position and drag events inside a container, record IDs and predictable DOM structure are essential for good DOM synchronization.

You also need to decide whether reordering is temporary or persistent. If the drag should only affect the screen, updating component state is enough. If the order must survive reloads, you should persist a sequence field or similar ordering value through the ORM or RPC layer after the drop event. Odoo’s JavaScript reference recommends using the orm service for model method calls from components.

useRef, setup(), and Draggable Selectors

The common pattern is to create a reference with useRef, assign it to the sortable container in your QWeb template, and initialize the hook inside setup(). The hook configuration then targets your draggable elements using a selector such as .o_draggable.

Typical options include enable, ref, elements, cursor, delay, tolerance, onWillStartDrag, and onDrop. These are especially helpful when you want a safer drag and drop interface, because delay and tolerance reduce accidental dragging, while callbacks let you control visual feedback and record reordering logic.

Step by Step useSortable Implementation

Here is a simple example for a custom reorderable task list in Odoo 19:

/** @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 SortableTaskList extends Component {
    static template = "my_module.SortableTaskList";

    setup() {
        this.orm = useService("orm");
        this.root = useRef("root");

        this.state = useState({
            items: [
                { id: 11, name: "Prepare demo", sequence: 10 },
                { id: 12, name: "Review specs", sequence: 20 },
                { id: 13, name: "Update module", sequence: 30 },
            ],
        });

        useSortable({
            enable: true,
            ref: this.root,
            elements: ".o_draggable",
            cursor: "move",
            delay: 150,
            tolerance: 8,
            onWillStartDrag: ({ element, addClass }) => {
                addClass(element, "opacity-75");
            },
            onDrop: (params) => this.onDropItem(params),
        });
    }

    async onDropItem({ element, previous }) {
        const draggedId = Number(element.dataset.id);
        const order = this.state.items.map((item) => item.id);

        const oldIndex = order.indexOf(draggedId);
        order.splice(oldIndex, 1);

        if (previous) {
            const previousId = Number(previous.dataset.id);
            const previousIndex = order.indexOf(previousId);
            order.splice(previousIndex + 1, 0, draggedId);
        } else {
            order.unshift(draggedId);
        }

        this.state.items.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));

        for (let index = 0; index < this.state.items.length; index++) {
            const item = this.state.items[index];
            item.sequence = (index + 1) * 10;
            await this.orm.write("my.task.model", [item.id], { sequence: item.sequence });
        }
    }
}

And the matching QWeb template:

 
<?xml version=”1.0″ encoding=”UTF-8″?>
<templates xml:space=“preserve”>
<t t-name=“my_module.SortableTaskList” owl=“1”>
<div t-ref=“root” class=“p-3 border rounded”>
<t t-foreach=“state.items” t-as=“item” t-key=“item.id”>
<div class=“o_draggable d-flex align-items-center p-2 mb-2 border rounded”
t-att-data-id=“item.id”>
<span class=“me-2 fa fa-bars”/>
<span t-esc=“item.name”/>
</div>
</t>
</div>
</t>

</templates>
 

This structure follows the usual Odoo frontend pattern: Owl component in JavaScript, QWeb template for the DOM, useRef for the sortable container, and service-based persistence for the sequence field.

Key Parameters and Event Callbacks

The most important part is understanding what each parameter does. elements tells the hook which nodes are draggable. delay and tolerance help protect normal clicks. onWillStartDrag is useful for adding temporary styling. onDrop is where you recalculate order and persist it if needed. Community examples for Odoo 19 also show a handle option when you want dragging to start only from a specific icon rather than the whole row.

One detail many developers miss is that the drop callback gives you enough positional information to rebuild the order using the dragged element and its previous sibling. That means you do not need a heavy drag library pattern just to update a local array. Keep the logic small, deterministic, and centered around IDs.

Midway through this kind of frontend work, it also helps to think beyond dragging itself. If you are also refining views and usability, Byte Legions’ guide on Customizing Odoo 19 UI for Better Team UX is a useful follow-up because it connects view structure, UX, and frontend customization in a practical way.

Keeping UI State, UX, and Record Order Stable

A smooth drag and drop interface is only half the job. The other half is making sure state management and persistence stay aligned. If your list reorders visually but the backend sequence field never changes, users will get confused after refresh or when other views load the same records. Using the ORM service after onDrop is the safest pattern when the order matters functionally.

For UX, a drag handle is often better than making the whole card draggable. It reduces accidental movement, especially in rows that also contain buttons, links, or inline edits. Combine that with delay and tolerance, and your sortable UI behavior becomes much more reliable in day to day use.

If you are implementing this inside a larger custom module, keep your sortable logic isolated and your ordering field explicit. That makes future maintenance easier, especially when the same record reordering has to appear in list reordering, Kanban sorting helpers, or custom side panels. Need help building that cleanly in a live Odoo 19 project? Book a Consultation.

Conclusion

The useSortable hook in Odoo 19 is a practical tool for building custom drag and drop features in Owl components. When you combine setup(), useRef, a clean QWeb template, and a backend sequence update, you get a sortable container that feels native to the Odoo web client instead of bolted on.

For Odoo developers and technical consultants, the key idea is simple: let the hook manage the interaction, let Owl manage the component lifecycle, and let your own drop logic manage the business meaning of the new order. That gives you a reusable foundation for many frontend customizations in Odoo 19.

Frequently Asked Questions (FAQs)

1. Can useSortable reorder database records automatically?

Not by itself. It can reorder the DOM and help you update local state, but if you want permanent record reordering you still need to write the new sequence field or ordering value back to the database through ORM or RPC logic.

2. Do I need useRef with useSortable?

In the common Odoo 19 pattern, yes. Developers typically use useRef to point the hook at the sortable container so the drag behavior is attached to the correct part of the QWeb template.

3. How can I use a drag handle instead of the whole item?

Use a dedicated icon or element as the handle and configure the hook accordingly. Community examples for Odoo 19 mention using a handle option when you want only a specific class or icon to trigger the drag.

4. How do I prevent accidental dragging or bad drops?

Use delay and tolerance in the hook configuration. These settings help distinguish a real drag action from a normal click or tiny cursor movement, which improves frontend event handling for busy interfaces.

5. What is the difference between useSortable and the handle widget in Odoo?

The handle widget is a standard view-level feature for existing ordered records, while useSortable is a frontend hook for custom Owl logic where you need more control over draggable elements, DOM structure, and drop behavior inside a custom component.

Visit our Odoo blog for more insights on improving your Odoo workflows, view customization, and overall business performance.

For businesses looking to improve frontend workflows and user experience, partnering with an odoo customization company in USA can make Odoo 19 custom development faster, cleaner, and easier to maintain.
For businesses looking to improve frontend workflows and user experience, partnering with an odoo customization company in USA can make Odoo 19 custom development faster, cleaner, and easier to maintain.

If you are building custom frontend behavior in Odoo 19, the useSortable hook is one of the cleanest ways to add drag and drop interaction inside Owl components. For teams working with custom views, embedded widgets, or reorderable action lists, it gives you practical control over sortable UI behavior without falling back to messy manual DOM code. If you are looking for experienced odoo erp consultants in USA to help with frontend customization, this is exactly the kind of technical work that benefits from a solid Odoo web client approach.

At a high level, Odoo’s frontend is built on Owl components, and those components are defined with QWeb templates. That matters because useSortable fits naturally into the same hook-based pattern you already use in setup(), useRef, and service-driven frontend event handling.

Understanding useSortable in Odoo 19

useSortable is used to attach drag and drop behavior to a sortable container so users can reorder draggable elements inside a custom Owl component. In practice, developers commonly import it from @web/core/utils/sortable_owl, initialize it inside setup(), and wire it to callbacks like onWillStartDrag and onDrop to react to user actions.

What makes it useful is not just the drag interaction itself. It also gives you a structured way to control list reordering, highlight the element being dragged, and decide how the new order should be stored in UI state or written back to the database. That makes it a strong fit for custom dashboards, embedded actions, Kanban sorting helpers, or any reorderable block inside the Odoo web client.

How It Fits into OWL and the Odoo Web Client

Odoo 19’s frontend framework treats hooks as reusable lifecycle-aware building blocks. Owl components provide the component model, QWeb template structure, and rendering layer, while hooks let you inject specific behavior into a component without bloating its class logic. useSortable sits neatly in that pattern.

A practical way to think about it is this: Owl handles rendering, QWeb defines the markup, and useSortable manages the drag behavior on top of that rendered DOM. So instead of manually binding and cleaning up listeners across patches and unmounts, you keep the logic inside the component lifecycle where it belongs.

Core Prerequisites Before Implementation

Before you add useSortable, make sure your component already has three basics in place: an Owl component, a QWeb template, and a stable list of items with unique identifiers. Since the hook works by tracking element position and drag events inside a container, record IDs and predictable DOM structure are essential for good DOM synchronization.

You also need to decide whether reordering is temporary or persistent. If the drag should only affect the screen, updating component state is enough. If the order must survive reloads, you should persist a sequence field or similar ordering value through the ORM or RPC layer after the drop event. Odoo’s JavaScript reference recommends using the orm service for model method calls from components.

useRef, setup(), and Draggable Selectors

The common pattern is to create a reference with useRef, assign it to the sortable container in your QWeb template, and initialize the hook inside setup(). The hook configuration then targets your draggable elements using a selector such as .o_draggable.

Typical options include enable, ref, elements, cursor, delay, tolerance, onWillStartDrag, and onDrop. These are especially helpful when you want a safer drag and drop interface, because delay and tolerance reduce accidental dragging, while callbacks let you control visual feedback and record reordering logic.

Step by Step useSortable Implementation

Here is a simple example for a custom reorderable task list in Odoo 19:

/** @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 SortableTaskList extends Component {
    static template = "my_module.SortableTaskList";

    setup() {
        this.orm = useService("orm");
        this.root = useRef("root");

        this.state = useState({
            items: [
                { id: 11, name: "Prepare demo", sequence: 10 },
                { id: 12, name: "Review specs", sequence: 20 },
                { id: 13, name: "Update module", sequence: 30 },
            ],
        });

        useSortable({
            enable: true,
            ref: this.root,
            elements: ".o_draggable",
            cursor: "move",
            delay: 150,
            tolerance: 8,
            onWillStartDrag: ({ element, addClass }) => {
                addClass(element, "opacity-75");
            },
            onDrop: (params) => this.onDropItem(params),
        });
    }

    async onDropItem({ element, previous }) {
        const draggedId = Number(element.dataset.id);
        const order = this.state.items.map((item) => item.id);

        const oldIndex = order.indexOf(draggedId);
        order.splice(oldIndex, 1);

        if (previous) {
            const previousId = Number(previous.dataset.id);
            const previousIndex = order.indexOf(previousId);
            order.splice(previousIndex + 1, 0, draggedId);
        } else {
            order.unshift(draggedId);
        }

        this.state.items.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));

        for (let index = 0; index < this.state.items.length; index++) {
            const item = this.state.items[index];
            item.sequence = (index + 1) * 10;
            await this.orm.write("my.task.model", [item.id], { sequence: item.sequence });
        }
    }
}

And the matching QWeb template:

 
<?xml version=”1.0″ encoding=”UTF-8″?>
<templates xml:space=“preserve”>
<t t-name=“my_module.SortableTaskList” owl=“1”>
<div t-ref=“root” class=“p-3 border rounded”>
<t t-foreach=“state.items” t-as=“item” t-key=“item.id”>
<div class=“o_draggable d-flex align-items-center p-2 mb-2 border rounded”
t-att-data-id=“item.id”>
<span class=“me-2 fa fa-bars”/>
<span t-esc=“item.name”/>
</div>
</t>
</div>
</t>

</templates>
 

This structure follows the usual Odoo frontend pattern: Owl component in JavaScript, QWeb template for the DOM, useRef for the sortable container, and service-based persistence for the sequence field.

Key Parameters and Event Callbacks

The most important part is understanding what each parameter does. elements tells the hook which nodes are draggable. delay and tolerance help protect normal clicks. onWillStartDrag is useful for adding temporary styling. onDrop is where you recalculate order and persist it if needed. Community examples for Odoo 19 also show a handle option when you want dragging to start only from a specific icon rather than the whole row.

One detail many developers miss is that the drop callback gives you enough positional information to rebuild the order using the dragged element and its previous sibling. That means you do not need a heavy drag library pattern just to update a local array. Keep the logic small, deterministic, and centered around IDs.

Midway through this kind of frontend work, it also helps to think beyond dragging itself. If you are also refining views and usability, Byte Legions’ guide on Customizing Odoo 19 UI for Better Team UX is a useful follow-up because it connects view structure, UX, and frontend customization in a practical way.

Keeping UI State, UX, and Record Order Stable

A smooth drag and drop interface is only half the job. The other half is making sure state management and persistence stay aligned. If your list reorders visually but the backend sequence field never changes, users will get confused after refresh or when other views load the same records. Using the ORM service after onDrop is the safest pattern when the order matters functionally.

For UX, a drag handle is often better than making the whole card draggable. It reduces accidental movement, especially in rows that also contain buttons, links, or inline edits. Combine that with delay and tolerance, and your sortable UI behavior becomes much more reliable in day to day use.

If you are implementing this inside a larger custom module, keep your sortable logic isolated and your ordering field explicit. That makes future maintenance easier, especially when the same record reordering has to appear in list reordering, Kanban sorting helpers, or custom side panels. Need help building that cleanly in a live Odoo 19 project? Book a Consultation.

Conclusion

The useSortable hook in Odoo 19 is a practical tool for building custom drag and drop features in Owl components. When you combine setup(), useRef, a clean QWeb template, and a backend sequence update, you get a sortable container that feels native to the Odoo web client instead of bolted on.

For Odoo developers and technical consultants, the key idea is simple: let the hook manage the interaction, let Owl manage the component lifecycle, and let your own drop logic manage the business meaning of the new order. That gives you a reusable foundation for many frontend customizations in Odoo 19.

Frequently Asked Questions (FAQs)

1. Can useSortable reorder database records automatically?

Not by itself. It can reorder the DOM and help you update local state, but if you want permanent record reordering you still need to write the new sequence field or ordering value back to the database through ORM or RPC logic.

2. Do I need useRef with useSortable?

In the common Odoo 19 pattern, yes. Developers typically use useRef to point the hook at the sortable container so the drag behavior is attached to the correct part of the QWeb template.

3. How can I use a drag handle instead of the whole item?

Use a dedicated icon or element as the handle and configure the hook accordingly. Community examples for Odoo 19 mention using a handle option when you want only a specific class or icon to trigger the drag.

4. How do I prevent accidental dragging or bad drops?

Use delay and tolerance in the hook configuration. These settings help distinguish a real drag action from a normal click or tiny cursor movement, which improves frontend event handling for busy interfaces.

5. What is the difference between useSortable and the handle widget in Odoo?

The handle widget is a standard view-level feature for existing ordered records, while useSortable is a frontend hook for custom Owl logic where you need more control over draggable elements, DOM structure, and drop behavior inside a custom component.

Visit our Odoo blog for more insights on improving your Odoo workflows, view customization, and overall business performance.

Comments are closed