---
title: "JavaScript Gantt"
description: "The LemonadeJS Gantt block for JavaScript: Percent-positioned gantt: drag to move and resize, dependencies, milestones. Contract-verified, framework-agnostic, zero dependencies, with a live example."
source: https://lemonadejs.com/docs/plugins/gantt/
---

<link rel="stylesheet" href="/v6/gantt.css">

# 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 <Gantt> per row div, same range, done:

  `<tr>``<td>`Design`</td>``<td>``<div>`<!-- <Gantt header=false ...> -->`</div>``</td>``</tr>`
  `<tr>``<td>`Build`</td>` `<td>``<div>`<!-- <Gantt header=false ...> -->`</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

<!--example-->

```js
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

```bash
npm install @lemonadejs/gantt
```

```js
import Gantt from '@lemonadejs/gantt';
import '@lemonadejs/gantt/style.css';
```

Three deployment forms, one component:

```js
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 commit
- `onclick` — (task, event)
- `onlink` — (fromTask, toTask) a dependency was drawn
- `onunlink` — (fromTask, toTask) a dependency was removed

## API

```js
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:

```js
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](/docs/v5/plugins/).