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
<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:
- In a div: the full chart — header timeline + one row per task (non-headless by default).
- OVER A TABLE: set
tableto 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
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.
| Prop | Type | Default | Description |
|---|---|---|---|
data | array | — | GanttTask[] BY REFERENCE (mutate + touch()) |
start | string | '' | viewport start (default: earliest task - 2 days) |
end | string | '' | viewport end (default: latest task + 2 days) |
header | boolean | true | timeline header (months + day ticks) |
grid | boolean | true | weekend shading + day grid (standalone look) |
rowheight | number | 36 | |
today | boolean | true | the today line |
editable | boolean | false | drag to move, edges to resize |
readonly | boolean | false | host override: view-only regardless of editable (clicks/pan still work) |
disabled | boolean | false | blocks ALL interaction (clicks, pan, edits) and dims the chart |
snap | number | 1 | drag snapping, in days |
table | string | '' | 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 commitonclick— (task, event)onlink— (fromTask, toTask) a dependency was drawnonunlink— (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.