JavaScript Gantt

@lemonadejs/gantt · ✓ 30 contract checks · framework-agnostic · zero dependencies

<Gantt /> — a simple, beautiful gantt chart designed to be EMBEDDED: bars are positioned in PERCENTAGES of a date range, never pixels, so any number of instances sharing the same start/end stay perfectly aligned regardless of container width. That makes the table case trivial — one headerless per row div, same range, done:

<tr>``<td>Design</td>``<td>``<div></div>``</td>``</tr> <tr>``<td>Build</td> <td>``<div></div>``</td>``</tr>

Standalone mode adds the timeline header (months + days), weekend shading and the today line — same engine, same percentages.

  • tasks { label, start, end, color, progress, type, readonly }
  • milestones: type ‘milestone’ renders a diamond (explicit only — a task collapsed to one day stays a narrow, still-resizable bar)
  • editable: drag the bar to move, drag the edges to resize — day snapping, live preview, Escape cancels mid-drag, onchange(task, start, end) fires ONLY on commit and mutates YOUR task object
  • keyboard: bars are focusable buttons (label + dates in the accessible name); Ctrl/Alt+Arrow moves a snap step, Shift+Arrow resizes the end — the SAME commit path (and onchange) as drag. Enter/Space activates (onclick); the header pans with arrows
  • dates are ‘YYYY-MM-DD’ strings, all math in LOCAL time
  • data BY REFERENCE: mutate + touch() re-renders

TWO MOUNTING MODES:

  1. In a div: the full chart — header timeline + one row per task (non-headless by default).
  2. OVER A TABLE: set table to a CSS selector — the gantt injects one lane cell per tbody row (task i ↔ row i), %-aligned, drag editing included, and renders its own element as the timeline header you place above/beside the table. Unmount removes every injected cell — the table returns to its original state.

Example

live
import { html } from 'lemonadejs';
import Gantt from '@lemonadejs/gantt';

const App = (props, { state }) => {
    const tasks = state([
        { id: 'discovery', label: 'Discovery', start: '2026-09-01', end: '2026-09-04', progress: 100, color: '#45889c' },
        { id: 'design', label: 'Design', start: '2026-09-03', end: '2026-09-10', progress: 80, dependencies: ['discovery'] },
        { id: 'build', label: 'Build', start: '2026-09-09', end: '2026-09-22', progress: 45, color: '#578163', dependencies: ['design'] },
        { id: 'qa', label: 'QA', start: '2026-09-21', end: '2026-09-28', progress: 10, color: '#bd7f40', dependencies: ['build'] },
        { id: 'launch', label: 'Launch', start: '2026-09-30', end: '2026-09-30', type: 'milestone', dependencies: ['qa'] },
    ]);
    const note = state('Drag a bar to move it, drag its edges to resize.');

    return html`<div>
        <${Gantt} data="${tasks}" start="2026-08-30" end="2026-10-03" editable
            onchange="${(task, start, end) => (note.value = `${task.label}: ${start} → ${end}`)}"
            onclick="${(task) => (note.value = `${task.label} is ${task.progress || 0}% done`)}" />
        <p style="font-size:13px">${note}</p>
    </div>`;
};

Installation

npm install @lemonadejs/gantt
import Gantt from '@lemonadejs/gantt';
import '@lemonadejs/gantt/style.css';

Three deployment forms, one component:

html`<${Gantt} />`                       // by value (no registration)
setComponents({ Gantt });               // then <Gantt /> by name anywhere
createWebComponent(Gantt);              // <lm-gantt> in plain HTML/any framework

Props

Every declared prop arrives as a live state — pass a value for a snapshot or a state for a two-way live wire. Attribute strings are coerced to the declared type.

PropTypeDefaultDescription
dataarrayGanttTask[] BY REFERENCE (mutate + touch())
startstring''viewport start (default: earliest task - 2 days)
endstring''viewport end (default: latest task + 2 days)
headerbooleantruetimeline header (months + day ticks)
gridbooleantrueweekend shading + day grid (standalone look)
rowheightnumber36
todaybooleantruethe today line
editablebooleanfalsedrag to move, edges to resize
readonlybooleanfalsehost override: view-only regardless of editable (clicks/pan still work)
disabledbooleanfalseblocks ALL interaction (clicks, pan, edits) and dims the chart
snapnumber1drag snapping, in days
tablestring''CSS selector: inject lanes into that table’s rows

Events

All event names are lowercase (the platform convention — LJS-305 warns otherwise).

  • onchange — (task, start, end) on drag commit
  • onclick — (task, event)
  • onlink — (fromTask, toTask) a dependency was drawn
  • onunlink — (fromTask, toTask) a dependency was removed

API

import { ref } from 'lemonadejs';
const gantt = ref();
html`<${Gantt} ref="${gantt}" />`;
// gantt.current.getRange(...)  ·  gantt.current.setRange(...)
  • getRange()
  • setRange()

Styling

All classes follow the lm-gantt-* convention; visual variants are data-* attributes on the root. Override freely — there is no styling engine to fight.

Contract

The machine-readable schema ships with the package:

import contract from '@lemonadejs/gantt/contract.json';

verify.json carries the conformance proof produced by verify(Gantt).

Looking for the v5 plugin? See the archived v5 documentation.