Build a Custom Gantt Timeline in Odoo 18 with Vis.js
By Braincuber Team
Published on February 3, 2026
While Odoo Enterprise comes with a robust Gantt view, sometimes developers need a lightweight, highly customizable timeline solution for precise requirements, or they simply want to add timeline visualization to a custom dashboard client action. The Vis.js Timeline library is a perfect candidate for this—it's fast, interactive, and integrates beautifully with Odoo's OWL framework.
In this technical guide, we will build a "Wedding Planner Timeline" dashboard. Instead of relying on standard views, we'll create a standalone Client Action that fetches tasks from a specific project and displays them on an interactive timeline where you can zoom, scroll, and click to view details.
Module Structure
We'll create a module named wedding_planner_visual. Here is the file structure we need:
You need to download vis-timeline-graph2d.min.js and vis-timeline-graph2d.min.css from the Vis.js website and place them in your module's static/lib/vis/ folder. Alternatively, you can use a CDN link in your manifest, but local files are recommended.
Step 1: The OWL Component
This component is the core of our Gantt view. It fetches tasks using the ORM service and initializes the Vis Timeline instance when the component mounts.
/** @odoo-module **/
import { Component, onMounted, useRef, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
export class WeddingTimeline extends Component {
static template = "wedding_planner_visual.TimelineTemplate";
setup() {
this.orm = useService("orm");
this.action = useService("action");
this.timelineRef = useRef("timeline-visual");
this.state = useState({
loading: true,
tasks: [],
});
onMounted(async () => {
await this.fetchTasks();
this.renderTimeline();
this.state.loading = false;
});
}
async fetchTasks() {
// Fetch tasks from a specific project (e.g., Wedding Project)
// Adjust the domain to match your real project criteria
this.state.tasks = await this.orm.searchRead(
"project.task",
[['project_id', 'ilike', 'Wedding']],
["id", "name", "date_deadline", "create_date", "user_ids", "stage_id"]
);
}
renderTimeline() {
if (!this.timelineRef.el) return;
// Prepare data for Vis.js
const items = this.state.tasks.map(task => ({
id: task.id,
content: task.name,
start: task.create_date, // or scheduled start if available
end: task.date_deadline || new Date().toISOString(),
group: task.stage_id ? task.stage_id[1] : 'Unassigned',
className: 'timeline-item-custom'
}));
// Create groups based on Stages
const groups = [...new Set(items.map(i => i.group))].map((g, index) => ({
id: g,
content: g
}));
const options = {
height: '600px',
stack: true,
horizontalScroll: true,
zoomKey: 'ctrlKey',
orientation: 'top',
start: new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
end: new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000), // 14 days ahead
};
// Initialize Vis Timeline
// We assume 'vis' is available globally or via module shim
// If loaded via assets, it attaches to window.vis
this.timeline = new window.vis.Timeline(
this.timelineRef.el,
new window.vis.DataSet(items),
new window.vis.DataSet(groups),
options
);
// Add event listener
this.timeline.on('select', (properties) => {
if (properties.items.length > 0) {
this.openTask(properties.items[0]);
}
});
}
openTask(taskId) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: "project.task",
res_id: taskId,
views: [[false, "form"]],
target: "current",
});
}
}
registry.category("actions").add("wedding_timeline_view", WeddingTimeline);
Step 2: The Template
The XML template is simple—it just needs a container for our timeline. We'll use a t-ref to access the DOM element in our JS code.
<templates xml:space="preserve">
<t t-name="wedding_planner_visual.TimelineTemplate" owl="1">
<div class="o_wedding_timeline h-100 d-flex flex-column">
<div class="timeline-header p-3 bg-white border-bottom d-flex justify-content-between align-items-center">
<h3 class="m-0">Wedding Project Timeline</h3>
<span t-if="state.loading" class="badge bg-warning">Loading...</span>
</div>
<div class="timeline-container flex-grow-1 position-relative">
<div t-ref="timeline-visual" class="w-100 h-100"/>
</div>
</div>
</t>
</templates>
Step 3: Manifest and Assets
We need to register our component and load the Vis.js library. In Odoo 18, we use the assets dictionary in the manifest.
{
'name': 'Wedding Planner Visuals',
'version': '1.0',
'depends': ['project', 'web'],
'data': [
'views/actions.xml',
],
'assets': {
'web.assets_backend': [
# Vis.js Library
'wedding_planner_visual/static/lib/vis/vis-timeline-graph2d.min.css',
'wedding_planner_visual/static/lib/vis/vis-timeline-graph2d.min.js',
# Our Custom Component
'wedding_planner_visual/static/src/xml/timeline_component.xml',
'wedding_planner_visual/static/src/js/timeline_component.js',
],
},
}
Step 4: The Menu Action
Finally, create the menu item that triggers our Client Action.
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_wedding_timeline" model="ir.actions.client">
<field name="name">Wedding Timeline</field>
<field name="tag">wedding_timeline_view</field>
</record>
<menuitem id="menu_wedding_timeline"
name="Timeline Dashboard"
parent="project.menu_main_pm"
action="action_wedding_timeline"
sequence="50"/>
</odoo>
Result and Interaction
When you reload your Odoo instance and navigate to the Project app, you'll see a new "Timeline Dashboard" menu. Clicking it loads your custom React-like OWL component.
Auto-Grouping
The code automatically groups tasks by their Stage (e.g., Todo, In Progress), creating horizontal swimlanes for better organization.
Interactivity
We added a click listener (this.timeline.on('select')) that opens the actual Task Form view when you click a timeline item, creating a seamless navigation experience.
Performance
Since we only fetch data once and render it on the client side, interacting with the timeline (zooming/panning) is instant and doesn't require server roundtrips.
This is just the beginning. You can extend this by allowing drag-and-drop rescheduling (updating the task date via the timeline's onMove event), coloring items based on priority, or adding dependencies drawn as lines between items.
Frequently Asked Questions
The standard Odoo Gantt view is excellent but is part of the Enterprise edition. Vis.js is a free, open-source library that allows developers to create custom timelines in the Community edition or when they need highly specific interactive behaviors (like custom grouping, complex styling, or specific drag-and-drop logic) that standard views don't support.
Vis.js provides `onMove` and `onMoving` events. You can attach a listener to these events in your OWL component. When an item is moved, you get the new start and end times. You can then use the Odoo ORM service (`this.orm.write`) to update the task record in the backend with the new dates.
Technically yes, you can link to CDN URLs in your XML templates or manifest. However, it is strongly recommended to download the libraries into your module's `static/lib` folder. This ensures your module works offline, isn't dependent on external servers, and avoids potential security or versioning issues.
A Client Action is a completely custom interface rendered in the web client, controlled entirely by JavaScript (OWL). Unlike standard Views (List, Form, Kanban) which are tied to specific Models and follow strict layouts, a Client Action gives you a blank canvas to build dashboards, custom reports, or specialized tools like this timeline.
In the `fetchTasks` method of your OWL component, you can add dynamic domains to the `this.orm.searchRead` call. You could add UI inputs (like a dropdown for Users or Stages) to your component's template, bind them to the state, and re-fetch tasks with the new criteria whenever the inputs change.
