I needed a free Gantt chart for two real projects. One was a weekend hack night. The other was a school play schedule. I tested Frappe Gantt, dhtmlxGantt (GPL), and jsGanttImproved. I built real pages. I pushed them hard. I broke a few things. Then I fixed them.
If you’d like to see every benchmark screenshot and heap profile from that test run, I tucked them into a full lab notebook right here.
You know what? I have clear winners for each use case.
Project one: a small status board with Frappe Gantt (MIT)
I made a one-page status board for a volunteer sprint. Plain HTML. Vite build. No framework. I wanted fast setup and a clean look.
Here’s the exact code I used to get it on the page:
<div id="gantt"></div>
<script type="module">
import Gantt from 'frappe-gantt';
const tasks = [
{
id: 'DESIGN',
name: 'Design mockups',
start: '2025-03-01',
end: '2025-03-04',
progress: 40,
dependencies: '',
custom_class: 'bar-design'
},
{
id: 'API',
name: 'Build API',
start: '2025-03-03',
end: '2025-03-10',
progress: 20,
dependencies: 'DESIGN',
custom_class: 'bar-api'
},
{
id: 'QA',
name: 'QA and polish',
start: '2025-03-08',
end: '2025-03-12',
progress: 0,
dependencies: 'API'
}
];
const gantt = new Gantt('#gantt', tasks, {
view_mode: 'Week',
date_format: 'YYYY-MM-DD',
on_click: task => console.log('clicked', task.id),
on_date_change: (task, start, end) => console.log('moved', task.id, start, end),
custom_popup_html: task => `
<div class="popup">
<h3>${task.name}</h3>
<p>${task.start} → ${task.end}</p>
<p>Progress: ${task.progress}%</p>
</div>`
});
</script>
<style>
#gantt { max-width: 900px; margin: 20px auto; }
.bar-design { fill: #4f46e5; }
.bar-api { fill: #059669; }
.gantt .bar.overdue { stroke: #dc2626; stroke-width: 2px; }
</style>
What I liked:
- It took me about 90 minutes from zero to pretty. That’s quick.
- The bars look good out of the box. The popup is easy to tweak.
- The API is simple. I could teach it in one coffee break.
What bugged me:
- With 400+ tasks, the scroll got laggy on my old laptop.
- Month view labels got cramped. I had to bump font size and spacing.
- No built-in critical path. No resource calendar. That’s fine for small stuff.
React note: I also wired it into a tiny React page. I had to use a ref and rebuild on prop changes. It worked, but it re-renders the whole chart. Here’s the piece I used:
import { useEffect, useRef } from 'react';
import Gantt from 'frappe-gantt';
export default function SprintGantt({ tasks, view = 'Week' }) {
const el = useRef(null);
const ganttRef = useRef(null);
useEffect(() => {
if (!el.current) return;
if (ganttRef.current) el.current.innerHTML = '';
ganttRef.current = new Gantt(el.current, tasks, { view_mode: view });
}, [tasks, view]);
return <div ref={el} style={{ maxWidth: 900, margin: '0 auto' }} />;
}
So, small project? Frappe Gantt felt smooth and light. It did the job.
Project two: heavier needs with dhtmlxGantt (GPL)
For a client, I had real PM needs. Links. Baselines. Drag and drop. Keyboard moves. Big data. I reached for dhtmlxGantt. It’s free under GPL, which means your app also needs to be GPL if you ship it as part of a product. That part matters. We were fine, since it was an internal tool.
Setup felt like a real tool. More knobs. More power.
I broke out an even more detailed, code-heavy walkthrough of this library—including auto-scheduling edge cases—in my hands-on review over here.
<div id="gantt_here" style="width:100%; height:500px;"></div>
<script>
gantt.config.xml_date = "%Y-%m-%d";
gantt.config.columns = [
{ name: "text", label: "Task", width: 200, tree: true },
{ name: "start_date", label: "Start", align: "center" },
{ name: "duration", label: "Days", align: "center" },
{ name: "add", label: "", width: 44 }
];
gantt.plugins({ tooltip: true, auto_scheduling: true });
gantt.init("gantt_here");
gantt.parse({
data: [
{ id:1, text:"Design", start_date:"2025-03-01", duration:4, progress:0.4 },
{ id:2, text:"API", start_date:"2025-03-05", duration:6, progress:0.2, parent:0 },
{ id:3, text:"QA", start_date:"2025-03-11", duration:3, parent:0 }
],
links: [
{ id:1, source:1, target:2, type:"0" }, // finish-to-start
{ id:2, source:2, target:3, type:"0" }
]
});
// Baseline example
gantt.addTaskLayer(function drawBaseline(task) {
if (!task.baseline_start || !task.baseline_end) return false;
const sizes = gantt.getTaskPosition(task, task.baseline_start, task.baseline_end);
const el = document.createElement('div');
el.className = 'baseline';
el.style.left = sizes.left + 'px';
el.style.width = sizes.width + 'px';
el.style.top = sizes.top + gantt.config.bar_height + 6 + 'px';
return el;
});
</script>
<style>
.baseline { position: absolute; height: 6px; background: #c084fc; border-radius: 3px; opacity: .6; }
</style>
What I liked:
- Linking tasks by dragging felt great. Super clear.
- It handled 1,200 tasks on my 2019 MacBook Air without drama.
- I liked the columns, keyboard support, and auto scheduling.
- Tooltips and baselines were clean to set up.
What bugged me:
- GPL is not a fit for every product. If you ship closed source, this is a no.
- The CSS is heavy. I spent time to make it match our brand.
- It took me longer to learn the config. The docs are good, though.
If you need heavy PM features, this one’s the grown-up in the room.
Quick mention: jsGanttImproved
I used this for a school play schedule. Cast, rehearsals, props. Nothing fancy. It felt old-school, but it loaded fast and did the basics.
In case you’re hunting specifically for cost-free options, I cataloged every truly free Gantt library I could find—including jsGanttImproved—in a roundup you can skim here.
What worked:
- No build step needed. Drop files, call a function.
- Good for read-only plans and simple dates.
What didn’t:
- Styling took more work.
- Features are basic. I missed nice tooltips and smooth drag.
If you ever outgrow these options and want a broader charting toolkit that still supports timelines, EJSChart is a flexible alternative worth bookmarking. For other visualization types beyond timelines, you can also check my comparison of open-source chart tools here.
Real hiccups I hit (and fixed)
- Time zones: The first day slipped by one day for a user in a DST zone. I fixed it by sending ISO dates (YYYY-MM-DD) and letting the chart parse them. No local Date math in my own code.
- Mobile: Touch targets were tiny near the chart edges. I added 6–8px padding around bars
