Gantt

Work on a timeline: drag a bar to reschedule and its edges to change the start or end, with a computed critical path and dependency conflicts surfaced as you create them.

src/components/ui/gantt.tsx

About

From the component's own source, where it was written.

Work on a timeline: bars for duration, dragged to reschedule.

**The view Kanban, Timeline and Stepper cannot give you.** Kanban shows state and hides time. A timeline shows moments, not spans. A stepper shows order, not overlap. Only this shows *how long* and *at the same time as what* — which is the entire question in scheduling.

**A read-only Gantt is a picture of a plan, and plans change.** Dragging is not decoration here: the whole reason to look at this chart is to notice that something has slipped, and the useful next action is to move it. A bar is dragged to reschedule, and its edges are dragged to change the start or the end independently.

**Everything snaps, and the snap is the unit the plan is kept in.** Dates dragged to arbitrary millisecond boundaries produce a plan nobody can read back; snapMinutes defaults to a day, which is the granularity most plans are actually managed at.

**Dragging is not the only way to move a task.** A bar is a real button: arrow keys move it a snap at a time, shift-arrow changes its end, and both announce through aria-valuetext. A chart that can only be edited with a pointer cannot be edited at all by a good number of people.

**It is monochrome, and meaning is carried by shape rather than hue.** A plan is read for structure — what is long, what overlaps, what is late — and a palette assigning arbitrary colours to tasks adds a legend to learn without adding information. It also means the two states that matter are not encoded in colour alone, which is the usual accessibility failure: the critical path is ringed and a dependency conflict is outlined in dashes, both legible to anyone who cannot separate red from grey.

**The critical path is computed, not annotated.** Highlighting it by hand is how a Gantt goes stale: someone slips a task, nobody re-marks the path, and the chart now points at the wrong risk. Dependency conflicts — a task starting before what it depends on ends — are surfaced as you create them, because an impossible schedule should look impossible while you are making it, not after you save.

Install

npx astralyx-ui add gantt

Usage

import { Gantt } from '@/components/ui/gantt'

<Gantt tasks={tasks} onChange={setTasks} criticalPath today={new Date()} />

Composer

Change the props and the source below follows.

Gantt chart

Apr 5Apr 11Apr 18Apr 25May 2May 9
Spec
Design
API
UI
Integration
QA
Docs
Launch

4 dependency conflicts: a task starts before what it depends on finishes.

Drag a bar to reschedule, its edges to change the start or end. Focus one and use the arrow keys — shift-arrow changes the end.

Props
tsx
<Gantt
  tasks={tasks}
  criticalPath={true}
  today={new Date()}
/>

Examples

A release plan, critical path marked

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

Gantt

Extends Omit<ComponentProps<'div'>, 'onSelect' | 'onChange'>.

PropTypeDefaultDescription
tasksGanttTask[]required{ id, label, start, end, progress?, dependsOn?, milestone? }.
onChange(tasks: GanttTask[]) => void—Supplying either makes the chart editable; omitting both makes it read-only. A read-only Gantt is a printout of a plan, and the reason to look at one is usually that something needs moving.
onTaskChange(task: GanttTask, previous: GanttTask) => void—Supplying either makes the chart editable; omitting both makes it read-only. A read-only Gantt is a printout of a plan, and the reason to look at one is usually that something needs moving.
onSelect(task: GanttTask) => void——
selectedIdstring——
snapMinutesnumber1440A day. Dragging to arbitrary millisecond boundaries produces a plan nobody can read back.
criticalPathbooleantrueThe longest dependency chain is derived from the data. Marking it by hand is how a Gantt goes stale — someone slips a task, nobody re-marks the path, and it points at the wrong risk.
labelWidthnumber160Column width for the task names.
rowHeightnumber34—
dateFormat(date: Date) => stringDEFAULT_DATE_FORMAT—
ticksnumber5Number of ticks along the top.
todayDate—A vertical marker. Omitted by default rather than assumed.
emptyLabelstring'Nothing scheduled.'—
labelstring'Gantt chart'—

Types

GanttTask

Work on a timeline: bars for duration, dragged to reschedule.

FieldTypeDescription
idstring—
labelReactNode—
startDate | string—
endDate | string—
progress?number0–1. Drawn as a fill inside the bar.
dependsOn?string[]Ids this task cannot start before.
group?string—
milestone?boolean—
locked?booleanExcluded from dragging and from keyboard editing.

Behaviour

TopicDetailDescription
vs Kanban / Timeline / Stepperthe missing viewKanban shows state and hides time. A timeline shows moments, not spans. A stepper shows order, not overlap. Only this shows how long, and at the same time as what.
draggingbody and edgesThe body moves the task and keeps its duration; the edges change the start and the end independently. Handles are 8px, which is the difference between resizing and moving by accident.
keyboardarrows and shift-arrowArrows move by one snap, shift-arrow changes the end, and the dates are announced through aria-valuetext. A chart editable only by pointer is not editable at all for a good number of people.
conflictsliveRecomputed against the drag rather than the saved data, so an impossible schedule looks impossible while you are making it.
monochromeshape, not hueA plan is read for structure — what is long, what overlaps, what is late — and arbitrary per-task colour adds a legend without adding information. The critical path is ringed and a conflict is outlined in dashes, so neither state is carried by colour alone.