I tried three open-source JavaScript Gantt charts. Here’s what actually worked.

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