Gantt

A tree-and-timeline chart: a sticky column pane on the left, a scrollable time grid on the right. Groups roll their children up into a summary track, overlapping bars stack into lanes, and the scale switches between hourly and yearly columns.

Bug Tracking
Mobile crash
Release 2.4
July 2026
ARTVLF+1
Name
Status
Assignee
Due date
W27 Jun 28 - Jul 4
W28 Jul 5 - 11
W29 Jul 12 - 18
W30 Jul 19 - 25
W31 Jul 26 - Aug 1
Wed 1
Thu 2
Fri 3
Sat 4
Sun 5
Mon 6
Tue 7
Wed 8
Thu 9
Fri 10
Sat 11
Sun 12
Mon 13
Tue 14
Wed 15
Thu 16
Fri 17
Sat 18
Sun 19
Mon 20
Tue 21
Wed 22
Thu 23
Fri 24
Sat 25
Sun 26
Mon 27
Tue 28
Wed 29
Thu 30
Fri 31
Bug Tracking
In Progress
Login redirect
Done
ARTV
Jul 15
Access rules
In Progress
KM
Jul 20
Mobile crash
In Progress
ARLFKM
iOS audit
In Progress
LF
Jul 22
Android audit
To Do
Jul 28
Button truncation
In Review
TV
Jul 16
Offline sync
To Do
ARLF
Jul 27
API key rotation
Done
KM
Jul 14
Session replay
To Do
Release 2.4
In Progress
Data export
In Progress
ARTV
Jul 22
Form validation
To Do
LF
Jul 27
Nav focus
To Do
Jul 29
Perf audit
In Review
KM
Jul 22
Ship 2.4
To Do
Jul 31
55%
Login redirect
Access rules
22%
iOS audit
Android audit
Button truncation
Offline sync
API key rotation
32%
Data export
Form validation
Nav focus
Perf audit
Ship 2.4

Bars are coloured by workstream, so a colour always means the same thing. Group rows show a rolled-up span with duration-weighted progress; Ship 2.4 is a milestone.

Gantt

A tree-and-timeline chart with a sticky column pane, group rollups, overlap lanes, milestones and switchable time scales.

Usage

import { Gantt } from "@/components/ui/gantt"
import type { GanttColumn, GanttRow } from "@/components/ui/gantt"

type Task = { status: string; owner: string }

const columns: GanttColumn<Task>[] = [
  { id: "name", header: "Name", width: 230, cell: (row) => row.label },
  { id: "status", header: "Status", width: 110, cell: (row) => row.data?.status },
]

const rows: GanttRow<Task>[] = [
  {
    id: "epic-1",
    label: "Bug Tracking",
    children: [
      {
        id: "task-1",
        label: "Login redirect",
        data: { status: "Done", owner: "Ana" },
        bars: [{
          id: "task-1-bar",
          start: new Date(2026, 6, 13),
          end: new Date(2026, 6, 15),
          label: "Login redirect",
          color: "blue",
          progress: 1,
        }],
      },
    ],
  },
]

<Gantt<Task>
  rows={rows}
  columns={columns}
  defaultScale="month"
  defaultDate={new Date(2026, 6, 22)}
  onBarClick={(bar, row) => console.log(bar.id, row.id)}
/>

Props

PropTypeDefaultDescription
rows*GanttRow<T>[]Row tree. A row with children renders as a collapsible group.
columns*GanttColumn<T>[]Left pane columns. The first one automatically gets the indent and expand/collapse chevron.
scale"day" | "week" | "month" | "quarter" | "year"Controlled time preset. Picks both the visible range and the column unit.
defaultScaleGanttScale"month"Uncontrolled initial scale.
onScaleChange(scale: GanttScale) => voidFires when the toolbar scale picker changes.
scalesGanttScale[]all fivePresets offered by the toolbar picker. A single-entry array hides it.
dateDateControlled anchor date. The visible range is derived from it.
defaultDateDatenew Date()Uncontrolled initial anchor date.
onDateChange(date: Date) => voidFires on Today and the prev/next page buttons.
nowDate | nullcurrent timePosition of the red now line. Pass null to hide it. Only rendered after mount.
weekStartsOn0 | 1 | ... | 60First day of the week — affects week columns, week grouping and the week range.
titleReactNodeOverrides the derived range label in the toolbar.
toolbarbooleantrueRenders the Today / scale / paging / title bar.
actionsReactNodeRendered at the right end of the toolbar.
zoomControlsbooleantrueFloating +/- control that scales column width from 0.5× to 4×.
rowHeightnumber36Height of a single-lane row, in px.
laneHeightnumber24Extra height each additional overlap lane adds to a row.
nonWorking(start: Date, end: Date) => booleanweekendsShades a column as non-working. Defaults to weekends on day columns, nothing otherwise.
summaryRowsbooleantrueGroup rows render a rollup track spanning their descendants, with duration-weighted progress.
borderedbooleantrueWraps the chart in a rounded border.
heightnumber | string520Scroll viewport height. A number is treated as px.
onBarClick(bar: GanttBar, row: GanttRow<T>) => voidMakes bars interactive and fires on click.
onRowClick(row: GanttRow<T>) => voidFires when either pane of a row is clicked.
onExpandedChange(expanded: Record<string, boolean>) => voidFires when a group is expanded or collapsed.
emptyStateReactNode"No rows to show"Shown when rows is empty.
classNamestringMerged onto the outer wrapper.

GanttRow<T>

PropTypeDefaultDescription
id*stringUnique row key.
label*stringRow name, available to every column renderer.
barsGanttBar[]Bars on this row. Overlapping bars are packed into lanes and the row grows taller.
childrenGanttRow<T>[]Nested rows. Presence of children makes the row a group.
defaultExpandedbooleantrueStart the group collapsed with false.
dataTArbitrary payload handed back to every column's cell renderer.

GanttBar

PropTypeDefaultDescription
id*stringUnique bar key.
start*DateBar start time.
end*DateBar end time. Ignored when milestone is true.
labelstringPlaced inside the bar when it fits, otherwise just after it.
labelPosition"auto" | "inside" | "outside" | "none""auto"Overrides the automatic label placement.
progressnumber0–1. Fills that fraction of the bar with the saturated hue.
colorGanttColor"neutral"One of the eight validated identity hues (blue, emerald, violet, amber, rose, teal, orange, indigo) or neutral.
variant"soft" | "solid" | "outline""soft"soft is a tint with a saturated rail; outline is dashed, for planned or queued work.
startLabelstringSmall muted caption just before the bar's leading edge.
endLabelstringSmall muted caption after the trailing edge. Skipped when the main label lands outside.
iconReactNodeRendered before the label, inside the bar.
milestonebooleanfalseRenders a ring marker at start instead of a bar.
tooltipReactNodeReplaces the default label / range / progress hover card.
disabledbooleanfalseDims the bar and suppresses onBarClick.

GanttColumn<T>

PropTypeDefaultDescription
id*stringUnique column key.
header*ReactNodeHeader cell content.
widthnumber200 / 120Column width in px — 200 for the first column, 120 for the rest.
align"left" | "center" | "right""left"Text alignment. Ignored on the first (tree) column.
cell*(row, ctx) => ReactNodeCell renderer. ctx carries depth, isGroup, expanded and the group's rolled-up span.