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.
@garn/ui/ganttOn 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.
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.
Flat
No groups, day scale — a simple task list with weekend shading and milestones.
Dependencies
Dependencies
FS/SS dependency arrows chaining into a milestone, a violation flagged, weighted group rollups, and interactive drag-to-link.
Critical Path
The zero-float critical path highlighted, with a floating side task showing its total float as a trailing bar.
Planning
Baseline Health
Planned-vs-actual baselines (ghost bars) + schedule-health tones (behind / overdue) around today's line.
Rail
Wbs Rail
The left rail as a WBS grid — nested subtasks, extra columns, a resizable divider, inline rename and drag-to-reorder.
Lenses
Lenses
Re-group by team or status, filter to active, and spotlight a dependency chain on hover.
Interaction
External Drop
Drag an unscheduled chip from outside onto a row to schedule it (onExternalDrop).
States
The imperative apiRef (today / fit / expand / collapse) plus the loading and empty states.
Appearance
Tones
The bar tone × appearance range (soft / solid), plus a summary and a milestone.
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.
npx garn-ui add gantt- 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
| Selector | State | Description |
|---|---|---|
[data-slot=gantt][data-scale] | scale | The active time scale is reflected on the root as `data-scale` (day/week/month/quarter/year). |
[data-slot=gantt][data-editable] | editable | Present on the root when the gantt accepts move/resize/progress edits. |
[data-slot=gantt-bar][data-selected=true] | selected | The selected bar — a brand ring; also aria-current="true". |
[data-slot=gantt-bar][data-dragging=true] | dragging | A bar being moved / resized (in-place preview; raised + shadowed). |
[data-slot=gantt-bar][data-critical] | critical | A bar on the critical path (danger ring) when `criticalPath` is on. |
[data-slot=gantt-bar][data-health] | health | A task toned by schedule health — `behind` (warning) or `overdue` (danger) — when `scheduleHealth` is on. |
[data-slot=gantt-bar][data-dimmed] | dimmed | A bar off the spotlighted dependency chain (faded) when `spotlight` is on. |
| Property | Token | Tier |
|---|---|---|
dayWidth | --garn-gantt-day-base | component · identity |
rowHeight | --garn-gantt-row | component · identity |
railWidth | --garn-gantt-rail | component · identity |
railColumnWidth | --garn-gantt-col | component · identity |
headerTier | --garn-gantt-tier | component · identity |
viewport | --garn-gantt-viewport | component · identity |
surface | --garn-background | semantic |
hairline | --garn-border | semantic |
todayLine | --garn-danger-solid | semantic |
selectedRing | --garn-brand-solidbrandable | semantic |
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
- Keep
itemsin your own state and apply the move / resize / progress / reorder / link callbacks optimistically. - Model phases as
groupsor parent tasks (viaparent) so children roll up into a weighted summary bar. - Add
dependenciesfor a real Gantt; turn oncriticalPath(with deps) to surface the driving chain and float. - Give a bar a
baselineand turn onscheduleHealthto compare planned vs actual around the today-line. - Pass
range/defaultRange(or anchor your dates) for SSR-deterministic output; build a toolbar fromuseGanttor drive it viaapiRef.
- 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/appearanceand 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
The root exposes an accessible name describing the visible range (default: "Roadmap, {range}").
when always
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
The grid, header, today-line, minimap and dependency arrows are aria-hidden; a11y lives on the bars + the [ / ] jump.
when always
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
| Tab | Enter the bars; one bar is tabbable (roving tabindex). |
| ArrowDownArrowRight | Move to the next bar (down the rows). |
| ArrowUpArrowLeft | Move to the previous bar. |
| HomeEnd | First / last bar. |
| EnterSpace | Select / activate the focused bar (onItemClick). |
| ShiftArrowLeft | Reschedule the focused bar one day earlier (when editable). |
| ShiftArrowRight | Reschedule the focused bar one day later (when editable). |
| AltArrowLeft | Shorten the focused bar by one day (when editable). |
| AltArrowRight | Lengthen 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. |
| Escape | Cancel an in-progress pointer drag / link / reorder (no commit). |
- 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".