Building a Reusable Gantt Chart as a Web Component

Time-based data — whether schedules, deadlines, or resource assignments — benefits from visual representation. A Gantt chart turns abstract job dates into an immediately readable timeline. While building your own may sound involved, the core mechanics are approachable once you break them into discrete steps. This article walks through the foundation of a custom <gantt-chart> element, built with vanilla JavaScript and CSS Grid, that you can adapt to your own data model.

The target component supports two zoom levels — year/month and month/day — with a configurable start and end date. Jobs are rendered as draggable bars; moving one updates the underlying data structure. Three sample jobs illustrate the behavior in the monthly view. Because the code is written as ES modules, you'll need to serve it over HTTP rather than opening files directly from disk. The live-server npm package is a convenient option for local testing. All source files — index.html, index.js, VanillaGanttChart.js, YearMonthRenderer.js, and DateTimeRenderer.js — are split into renderer classes so the component stays portable.

Component Structure and Renderer Delegation

The outer shell is a custom element defined in VanillaGanttChart.js. Its constructor attaches a template, containing the static HTML and CSS, to the shadow DOM. The chart is initialized with two arrays: jobs (the tasks shown as bars) and resources (the row labels, which might be tasks, people, or machines). The YearMonthRenderer is the default; switching levels in the changeLevel method clears the old renderer's output and hands rendering to its counterpart, DateTimeRenderer.

The interaction between scripts is straightforward:

  • index.html holds the <gantt-chart> tag.
  • index.js instantiates the component with the required jobs and resources; you can use multiple chart instances this way.
  • The VanillaGanttChart element acts as a façade, delegating all drawing to either the YearMonthRenderer or DateTimeRenderer.

Grid Layout: The Calendar Structure

Rendering in YearMonthRenderer uses a constructor function, favoring var for private variables and methods like this.render for the public API. Its initialization runs through several focused methods:

  1. initSettings — draws the controls for picking the time range.
  2. initGantt — lays out the chart's grid in four passes: initFirstRow for month headers, initSecondRow for day cells, initGanttRows for per-resource rows, and initJobs to position the draggable bars.

CSS Grid handles the multi-column layout cleanly. The total number of columns is one fixed-width resource label column (e.g., 100px) plus one flexible column per month. Setting grid-template-columns: 100px repeat(N, 1fr) on the container gives you a dynamic layout where each month shares the remaining space equally.

The grid is filled row by row. The first row carries three div elements: a resource-label spacer and two month containers. The second row repeats this structure, but each month div is itself a mini-grid containing the individual days. The month rows also use display: grid, and since days per month vary between 28 and 31, grid-auto-columns: minmax(20px, 1fr) keeps each cell at a usable minimum while stretching them to fill the width. Subsequent rows for each resource are generated similarly, only with empty cells rather than day labels.

Positioning and Dragging Jobs

Each day cell in the chart area carries two data attributes: data-resource (for the row) and data-date (for the column). A job's initial position is found simply by using querySelector to locate the matching cell. Its width depends on the zoom level: in month/day view, a grid cell equals one day, so a 2-day job takes up 2 * 100% width. Setting the job's width via calc() makes it proportional to its duration.

Drag-and-drop requires three hooks:

  • draggable="true" on each job bar.
  • An ondragstart handler on the job to record its identity, typically writing to the data transfer object.
  • An ondrop handler on every target cell that reads the dragged job's data, moves it to the new cell, and updates its start date and resource. Firing this handler requires also calling preventDefault on ondragover for the cells.

In effect, the drop event mutates the job object's properties, and that instance is shared with the state outside the component.

Usage and Two-Way State Binding

Integrating the component means two things: importing VanillaGanttChart.js as a module on your page, and writing a separate setup script that passes the initial data. Your setup file picks the <gantt-chart> element and calls its render method, typically with new VanillaGanttChart(), then pushes the jobs and resources arrays into it.

Reflecting drag-and-drop changes back into your app's state uses JavaScript Proxy objects. Instead of passing the bare jobs array to the chart, you wrap each job in a Proxy equipped with a validator. The validator's set trap fires whenever a job's start, end, or resource property changes inside the component. By logging those changes (or wiring them to your data layer), you get a read on state mutations without manually checking each job after every drag. The get trap can likewise react to reads if needed. This proxy pattern keeps the chart decoupled from your application's internal state handling — the component just mutates the objects you gave it, and you decide how to respond.

The component's two renderer classes and proxy-driven state model give you a solid, customizable foundation for any scheduling interface — from simple project planning to complex resource allocation — all without pulling in a charting library.

Taking the Component Further

The Gantt chart built here demonstrates how Web Components, CSS Grid, and JavaScript Proxy can work together to create a custom HTML element with a non-trivial graphical interface. The implementation stays framework-agnostic, so you can extend it or drop it into projects that use other JavaScript libraries.

All sample files and running instructions are linked at the top of the article.

Smashing Editorial