Skip to content

Gantt

A horizontal roadmap / Gantt canvas — entity rows × a zoomable time axis with editable bars, milestones, rolled-up summaries, dependency arrows and a critical path.

Betav0.4.2added in v0.2.0@garn/ui/gantt
On this page

Default

The hero: a grouped roadmap with milestones, progress, snap, a minimap, hover detail and a full drag editor with a useGantt scale/zoom toolbar.

100%

Overview

Roadmap

The hero: a grouped roadmap with milestones, progress, snap, a minimap, hover detail and a full drag editor with a useGantt scale/zoom toolbar.

100%

Flat

No groups, day scale — a simple task list with weekend shading and milestones.

100%

Dependencies

Dependencies

FS/SS dependency arrows chaining into a milestone, a violation flagged, weighted group rollups, and interactive drag-to-link.

100%

Critical Path

The zero-float critical path highlighted, with a floating side task showing its total float as a trailing bar.

100%

Planning

Baseline Health

Planned-vs-actual baselines (ghost bars) + schedule-health tones (behind / overdue) around today's line.

100%

Rail

Wbs Rail

The left rail as a WBS grid — nested subtasks, extra columns, a resizable divider, inline rename and drag-to-reorder.

100%

Lenses

Lenses

Re-group by team or status, filter to active, and spotlight a dependency chain on hover.

100%

Interaction

External Drop

Drag an unscheduled chip from outside onto a row to schedule it (onExternalDrop).

100%

States

The imperative apiRef (today / fit / expand / collapse) plus the loading and empty states.

100%

Appearance

Tones

The bar tone × appearance range (soft / solid), plus a summary and a milestone.

100%

Installation

garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.

terminal
npx garn-ui add gantt
First time? Set up garn in your project
Registry
gantt
Deps
class-variance-authoritylucide-react@radix-ui/react-slot
Registry deps
utilsuse-controllable-stateuse-announceuse-isomorphic-layout-effectdrag-gestureinline-editemptyskeletonhover-card

Import

import { Gantt, GanttBar } from "@garn/ui/gantt";

Anatomy

The parts this component is built from — compose them to assemble it.

GanttRequired[data-slot="gantt"]

Root + provider (generic over the item type). Owns scale/range/collapse/selection + the drag machine, dependency resolution, critical-path/rollup math and the imperative apiRef; renders the rail + time canvas. Lifts useGantt for a consumer toolbar.

GanttBar[data-slot="gantt-bar"]

The default bar / summary / milestone renderer — tone × appearance (badge tokens) × variant (task / summary / milestone), a tonal left accent, progress fill and optional resize / progress / link handles. Exported for use inside renderItem.

Props

The component's public props and their types.

apiRef
Description
An imperative handle for driving the gantt from outside a child.
Type
React.Ref<GanttApi>
collapsedGroups
Description
Controlled collapsed group ids. Pair with `onCollapsedGroupsChange`.
Type
string[]
criticalPath
Description
Highlight the critical path (the zero-float dependency chain) + show each non-critical task's total float as a faint trailing bar. Needs `dependencies`.
Type
boolean
Default
false
defaultCollapsedGroups
Description
Initially-collapsed group ids when uncontrolled.
Type
string[]
defaultRange
Description
Initial visible window when uncontrolled. Defaults to a fit around the items.
Type
GanttRange
defaultScale
Description
Initial zoom scale when uncontrolled.
Type
GanttScale
Default
"week"
defaultSelectedId
Description
Initially-selected row id when uncontrolled.
Type
string
Default
null
dependencies
Description
Scheduling links drawn as arrows between bars (cycle-safe; see GanttDependency).
Type
GanttDependency[]
editable
Description
Allow dragging bars to reschedule/resize and dragging the progress handle.
Type
boolean
Default
false
empty
Description
What to render when there are no rows. Defaults to a built-in empty state.
Type
React.ReactNode
filter
Description
Hide rows that don't match; rollups stay honest over the visible set.
Type
(item: T) => boolean
firstDay
Description
Week start (0=Sun…6=Sat); defaults to the locale's convention, else Sunday.
Type
number
groupBy
Description
Re-group rows by a derived key (status, assignee, …), overriding `group`. Return a group `{ id, label, tone }`, a bare id string, or null for ungrouped.
Type
(item: T) => { id: string; label: string; tone?: GanttTone; } | string | null
groups
Description
Collapsible section headers; a row joins one via its `group` field.
Type
GanttGroup[]
isWorkingDay
Description
Predicate for column shading (day scale). Defaults to treating weekends as non-working.
Type
(date: Date) => boolean
itemsRequired
Description
The rows of work, each a dated bar. Gantt is generic over the item type.
Type
T[]
loading
Description
Show a skeleton loading state instead of the rows.
Type
boolean
Default
false
locale
Description
BCP-47 locale for header dates and week convention; defaults to the runtime locale.
Type
string
minimap
Description
Show a minimap / overview strip below the canvas with a draggable viewport.
Type
boolean
Default
false
onCollapsedGroupsChange
Description
Fired when a group is collapsed or expanded.
Type
(ids: string[]) => void
onDependencyCreate
Description
Enable interactive drag-to-link — hovering a bar shows edge handles you drag onto another bar to create a dependency. Fired only for cycle-safe links.
Type
(dep: Required<Pick<GanttDependency, "from" | "to" | "type">>) => void
onExternalDrop
Description
Accept a native (HTML5) drag-in onto a row; fires with the drop target.
Type
(info: { row: string | null; start: Date; end: Date; }) => void
onItemClick
Description
Fired when a bar is activated (click / Enter).
Type
(item: T) => void
onItemMove
Description
Fired after a drag-reschedule commits, with the item's new range.
Type
(item: T, range: GanttRange) => void
onItemProgress
Description
Fired after the progress handle is dragged, with the new fraction (0–1).
Type
(item: T, progress: number) => void
onItemRename
Description
Enable inline rename of a row's label in the rail (composes InlineEdit).
Type
(item: T, label: string) => void
onItemResize
Description
Fired after a drag-resize commits, with the item's new range.
Type
(item: T, range: GanttRange) => void
onRangeChange
Description
Fired when the visible window changes (zoom, fit, navigation).
Type
(range: GanttRange) => void
onReorder
Description
Enable dragging a rail row to reorder / reparent it. Fires with the drop target: the new parent (or null for top level) and index among its children.
Type
(item: T, target: { parent: string | null; index: number; }) => void
onScaleChange
Description
Fired when the user zooms to a different scale.
Type
(scale: GanttScale) => void
onSelectedChange
Description
Fired when the selected row changes.
Type
(id: string | null) => void
onVisibleRangeChange
Description
Fired as the canvas scrolls horizontally (the visible-day range). Named so it never shadows the native `onScroll` DOM prop (different signature).
Type
(info: { startDay: number; endDay: number; }) => void
railColumns
Description
Extra left-rail columns (turns the rail into a mini WBS grid).
Type
GanttRailColumn<T>[]
range
Description
Controlled visible date window. Pair with `onRangeChange`.
Type
GanttRange
ref
Description
No description.
Type
React.Ref<HTMLDivElement>
renderDetail
Description
Rich detail shown in a HoverCard when a bar is hovered/focused.
Type
(item: T) => React.ReactNode
renderItem
Description
Custom content inside each bar; receives the original item plus render context.
Type
(item: T, ctx: GanttItemContext) => React.ReactNode
resizableRail
Description
Let the user drag the rail / canvas divider to resize the rail.
Type
boolean
Default
false
scale
Description
Controlled zoom scale (day…year). Pair with `onScaleChange`.
Type
GanttScale
scheduleHealth
Description
Tone each task by schedule health (behind / overdue) from its progress vs today.
Type
boolean
Default
false
scrollToNow
Description
Scroll the canvas near today on mount (instead of opening at the span start).
Type
boolean
Default
false
selectable
Description
Allow selecting a row (click / arrow keys), reflected via `selectedId`.
Type
boolean
Default
false
selectedId
Description
Controlled selected row id (`null` = none). Pair with `onSelectedChange`.
Type
string
showWeekNumbers
Description
Show ISO week numbers on the week-scale header tier.
Type
boolean
Default
false
snap
Description
During a drag, snap the moving edge to nearby bar edges / today / milestones.
Type
boolean
Default
false
spotlight
Description
On hover, highlight a bar's full dependency chain and dim the rest.
Type
boolean
Default
false
todayLine
Description
Draw the live today marker across the grid.
Type
boolean
Default
true

Plus 277 inherited native <div> attributes.

GanttBar

asChild
Description
Render as the child element (a custom bar), merging bar props onto it.
Type
boolean
linkable
Description
Show the dependency link handles (Gantt sets this from `onDependencyCreate`).
Type
boolean
onLinkStart
Description
No description.
Type
(edge: "start" | "end", e: React.PointerEvent) => void
onProgressStart
Description
No description.
Type
(e: React.PointerEvent) => void
onResizeStart
Description
No description.
Type
(edge: "start" | "end", e: React.PointerEvent) => void
progressHandle
Description
Show the progress drag handle at the fill edge.
Type
boolean
ref
Description
No description.
Type
React.Ref<HTMLButtonElement>
resizable
Description
Show start/end resize handles (Gantt sets this from `editable`).
Type
boolean

Plus 291 inherited native HTML attributes.

Styling

Target these data-slots and states, and remap these tokens, to restyle without forking the component.

[data-slot="gantt"][data-slot="gantt-rail"][data-slot="gantt-rail-header"][data-slot="gantt-row-header"][data-slot="gantt-canvas"][data-slot="gantt-header"][data-slot="gantt-body"][data-slot="gantt-grid"][data-slot="gantt-today"][data-slot="gantt-deps"][data-slot="gantt-row"][data-slot="gantt-bar"][data-slot="gantt-summary"][data-slot="gantt-milestone"][data-slot="gantt-empty"][data-slot="gantt-minimap"]

States

SelectorState
[data-slot=gantt][data-scale]scale
[data-slot=gantt][data-editable]editable
[data-slot=gantt-bar][data-selected=true]selected
[data-slot=gantt-bar][data-dragging=true]dragging
[data-slot=gantt-bar][data-critical]critical
[data-slot=gantt-bar][data-health]health
[data-slot=gantt-bar][data-dimmed]dimmed
PropertyTokenTier
dayWidth--garn-gantt-day-basecomponent · identity
rowHeight--garn-gantt-rowcomponent · identity
railWidth--garn-gantt-railcomponent · identity
railColumnWidth--garn-gantt-colcomponent · identity
headerTier--garn-gantt-tiercomponent · identity
viewport--garn-gantt-viewportcomponent · identity
surface--garn-backgroundsemantic
hairline--garn-bordersemantic
todayLine--garn-danger-solidsemantic
selectedRing--garn-brand-solidbrandablesemantic

Private instance vars (never externalize): --garn-gantt-day--garn-gantt-rail--gantt-x--gantt-w--gantt-p--b-bg--b-fg--b-solid--b-solid-fg--b-line--b-fill--b-rail

When to use

Laying out and editing work against a horizontal time axis — a product roadmap, a project plan with dependencies and a critical path, a delivery/release schedule, a construction or campaign timeline, or a WBS with rolled-up summary bars.

Reach for something else when

  • A vertical, read-only chronological feed of events (use timeline)
  • an hour-grid day/week calendar with overlapping events (use scheduler)
  • picking a date or range (use date-picker / calendar)
  • a plain hierarchical list with no time axis (use tree / table).

Overview

Gantt is a roadmap canvas — entity rows on the left, a zoomable time axis across the top, and a horizontal bar per item from its start to its end. Because each item owns a row, there is no overlap packing; the axis stretches from a day view out to years. Zero-duration items render as milestone diamonds, and a group or parent task rolls its children into a summary bar with duration-weighted progress. It is a full editor: drag a bar to reschedule, drag its edges to resize, drag the progress handle — by pointer or keyboard — with optimistic callbacks, so you always own items. On top of the core sit the planning layers: dependencies draw cycle-safe FS/SS/FF/SF arrows (with drag-to-link), criticalPath runs a backward-pass CPM to ring the zero-float chain and show each task's float, baseline + scheduleHealth compare planned vs actual, and groupBy / filter / spotlight re-lens the same data. For a vertical read-only history use timeline; for an hour-grid calendar use scheduler.

Guidelines

Own the data; treat the callbacks as intents. items is a controlled data prop — onItemMove / onItemResize / onItemProgress / onReorder / onDependencyCreate hand you the proposed change; apply it to your own state. Nothing moves until you do.

Style only through tone × appearance and the render slot. Give bars a tone (the shared vocabulary) and appearance (solid / soft / outline); reach for renderItem when you need custom bar content and renderDetail for a hover peek. Geometry rides inline CSS variables in day-units — never hand-position or hardcode a color or a pixel length.

Keep it operable and honest. The keyboard model is the differentiator — arrow-navigate bars, Enter to select, Shift/Alt+arrows to reschedule/resize, [ and ] to walk dependencies; don't strip the focus ring or the sr-only instructions. Pass range/defaultRange (or a fixed anchor for your dates) for SSR-deterministic output.

Best practices

Do
  • Keep items in your own state and apply the move / resize / progress / reorder / link callbacks optimistically.
  • Model phases as groups or parent tasks (via parent) so children roll up into a weighted summary bar.
  • Add dependencies for a real Gantt; turn on criticalPath (with deps) to surface the driving chain and float.
  • Give a bar a baseline and turn on scheduleHealth to compare planned vs actual around the today-line.
  • Pass range/defaultRange (or anchor your dates) for SSR-deterministic output; build a toolbar from useGantt or drive it via apiRef.
Don't
  • Don't mutate the passed items in a callback and expect a re-render — update your source of truth.
  • Don't hardcode bar colors or positions — use tone / appearance and let the canvas place them in day-units.
  • Don't remove the composed bar accessible name inside a custom renderItem.
  • Don't expect garn to reschedule for you — dependencies and the critical path are display-only (garn flags violations, never moves bars).

Troubleshooting

Dragging or resizing a bar does nothing — it snaps back.

Cause. items is a controlled data prop; the callbacks are intents, not mutations. The component never changes your data for you.

Fix. In onItemMove / onItemResize / onItemProgress, apply the returned range/fraction to your own state (setItems(prev => prev.map(...))).

A dependency arrow doesn't appear, or the critical path is empty.

Cause. An endpoint is hidden inside a collapsed group/parent, an edge was dropped for closing a cycle, or criticalPath is on without dependencies.

Fix. Expand the endpoints; remember resolveDeps drops self-links, unknown ids and cycle-closing edges; pass dependencies for criticalPath to have a graph.

Bars won't move by pointer even though keyboard edits work.

Cause. Editing is off, or that item opted out. Pointer geometry needs the canvas laid out (it measures the gantt-canvas).

Fix. Set editable; check the item isn't editable: false and isn't a milestone (zero-duration, non-resizable). Keyboard Shift/Alt+arrows are layout-free and always work when editable.

The range shows the wrong window and won't update when items change.

Cause. The uncontrolled range seed is fitted once from the first items/scale; later item changes don't re-fit an uncontrolled range.

Fix. Control range (with onRangeChange) and re-fit yourself, or call apiRef.fitToItems(), or key the component to remount.

SSR hydration mismatch — the today-line or fitted range differs between server and client.

Cause. With no range/defaultRange the seed is fitted from new Date()-derived data, and the today-line reads the live clock.

Fix. Anchor your item dates and pass range/defaultRange for deterministic SSR; the today-line reconciles on mount.

A custom renderItem bar keeps the default GanttBar chrome (accent rail, progress fill, lozenge shape) around my content.

Cause. renderItem is a CONTENT slot rendered inside the GanttBar control (so it keeps the ref, roving tabindex, accessible name and drag/select wiring) — it is not a full replacement of the bar element.

Fix. Style within the bar via tone/appearance and the content you return; branch on ctx.variant (task/summary/milestone) for per-kind content. The bar's accessible name and interaction come for free — don't try to re-add them.

Accessibility

Role
group
ARIA APG
grid
Focus
Roving tabindex over the bars (and focusable group/parent summary bars); focus-visible rings preserved; keyboard focus follows [ / ] dependency jumps.

Accessibility requirements

warn

The root exposes an accessible name describing the visible range (default: "Roadmap, {range}").

when always

warn

Every focusable bar control has a composed accessible name including its label and dates; a custom renderItem must not remove it.

when a bar is rendered

warn

The grid, header, today-line, minimap and dependency arrows are aria-hidden; a11y lives on the bars + the [ / ] jump.

when always

warn

The collapse control is a button with aria-expanded and a label naming the group/task.

when a group or parent row is rendered

Keyboard

TabEnter the bars; one bar is tabbable (roving tabindex).
ArrowDownArrowRightMove to the next bar (down the rows).
ArrowUpArrowLeftMove to the previous bar.
HomeEndFirst / last bar.
EnterSpaceSelect / activate the focused bar (onItemClick).
ShiftArrowLeftReschedule the focused bar one day earlier (when editable).
ShiftArrowRightReschedule the focused bar one day later (when editable).
AltArrowLeftShorten the focused bar by one day (when editable).
AltArrowRightLengthen the focused bar by one day (when editable).
[Jump to the focused bar's predecessor (a dependency source).
]Jump to the focused bar's successor.
EscapeCancel an in-progress pointer drag / link / reorder (no commit).
A1.3.1Info and RelationshipsA2.1.1KeyboardA4.1.2Name, Role, ValueAA2.4.7Focus VisibleAA1.4.10Reflow
  • The root is role=group with an accessible name for the visible range (aria-roledescription="gantt chart") and an sr-only aria-describedby that states the keyboard model.
  • The decorative grid, time header, today-line, minimap and dependency arrows are presentational (aria-hidden); the real semantics live on the bars.
  • Each bar is a focusable control with a composed accessible name ("{label}, {start} to {end}, {n}% complete"); a milestone reads "{label} milestone, {date}", a summary "{label} summary, …".
  • The name also folds in relationships + status: "after {preds}", "blocks {succs}", "on the critical path", "overdue" / "behind schedule", and the baseline range.
  • One bar is tabbable at a time (roving tabindex); selection is exposed via aria-current="true" on the selected bar (not visual-only).
  • Group / parent rows have an aria-expanded collapse toggle labelled "Expand/Collapse {label}". The rail resize divider is a labelled role=separator with aria-value min/max/now; the minimap is a role=scrollbar with aria-controls the scroll region.
  • Move / resize / progress / link / reorder results are announced in a polite live region; a cancelled drag announces "Cancelled."
  • Geometry uses logical properties, so the rail, bars and arrows mirror under dir="rtl".