Skip to content
ilinxa/pro-ui

Gantt Timeline

alphav0.6.0

Editable Gantt timeline — per-task bars, collapsible summary rows, milestone diamonds, continuous zoom from hours to quarters, and a today line.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-06-20Author: ilinxa

Context

Gantt Timeline is the time-axis sibling of task-card (cards) and kanban-board (columns): it consumes the SAME canonical TaskItem type and lays it on a horizontal time axis, so a 'Timeline' tab is literally the same task data the List + Board tabs show, on a third surface. v0.2 makes it editable: opt in with `editable` and edits fire task-card-shaped events + onChange(TaskItem[]) for the controlled consumer to echo (no internal data state). The permission matrix (TaskPermissions) + per-action predicates are reused from task-card — the gantt is the third consumer after the card and task-tree. Use it for sprint/cycle planning, delivery roadmaps (collapse to epics), content embargo→sunset schedules, and agent/pipeline run windows. Compound structure: GanttTimelineRoot (headless provider) + flat parts (Toolbar/Axis/Gutter/Body) + Tier-C primitives (GanttBar/SummaryBar/MilestoneDiamond/TodayLine/GutterRow/AxisHeader/BarTooltip/GanttContextMenu) + the GanttTimeline assembly. The full-card tooltip and the edit editor lazy-load task-card, so the default read-only path keeps it out of the bundle.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/gantt-timeline

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/gantt-timeline-fixtures

Preview

Drag to pan · ⌘/ctrl-wheel or pinch to zoom · click a bar
ASQ3 product launchIn progress
LPDesign system auditDone
LPBuild component libraryIn progress
Buttons + inputsDone
Data tableIn progress
API contract sign-offTo do
MCInfrastructure hardeningIn progress
Migrate to new clusterBlocked
Load testingTo do
Decommission legacy stackTo do
SRMarketing site refreshIn progress
Public launchTo do

Demo source

demo.tsxtsx

Usage

When to use

Reach for GanttTimeline when you already render the canonical TaskItem[] as cards (task-card) or columns (kanban-board) and want a third surface that answers “what runs when, and what overlaps?”— a time axis the other surfaces can't provide. Editing (drag-to-reschedule, resize, create, delete, reparent, detail-edit) is opt-in via editable — off by default, so it stays a clean read-only timeline until you want more.

Basic example

import { GanttTimeline } from "@/components/gantt-timeline"

export function TasksTimeline({ tasks }) {
  return (
    <GanttTimeline
      data={tasks}                  // the same TaskItem[] as List + Board
      statusOptions={STATUS_OPTIONS}
      defaultZoom="week"
      onTaskClick={(t) => openTaskDetail(t.id)}
    />
  )
}

Composed (lighter)

Drop the assembly and hand-place the parts. GanttTimelineRoot holds all state + gestures; any subset of GanttTimelineGutter / GanttTimelineAxis / GanttTimelineBody wires itself through context — no prop-drilling.

<GanttTimelineRoot data={tasks} statusOptions={STATUS_OPTIONS} defaultZoom="day">
  <GanttTimelineAxis />
  <div className="flex h-105">
    <GanttTimelineGutter />
    <GanttTimelineBody />
  </div>
</GanttTimelineRoot>

Editing (v0.2–v0.4, opt-in)

Set editable and wire onChange — data is controlled, so you echo the mutated TaskItem[] back (and get undo/redo for free from a history stack). Drag bars to move, drag edges to resize, double-click or right-click to edit, drag the gutter grip to reparent, and drag a summary bracket to group-move the whole subtree (v0.3). To draw a new task, flip the toolbar Draw toggle (v0.4) and drag on an empty row — with Draw off, an empty-row drag pans instead. Gated by the same permissions matrix as task-card / task-tree.

<GanttTimeline
  data={tasks}
  editable
  onChange={setTasks}                 // controlled echo (source of truth)
  permissions={{ byItem: { "epic-1": { remove: false } } }}
  onItemMoved={(e) => persist(e)}
/>

Notes

  • Navigation: drag to pan (flick for momentum), pinch or ⌘/ctrl-wheel to zoom toward the cursor, plain wheel scrolls rows, shift-wheel pans. Toolbar + / − / Fit / Today and the imperative handle cover the same ground.
  • Bars: effective window is startAt ?? setAt expireAt ?? (start + duration); no end ⇒ a milestone diamond. Color is status-tone fill (done=gray, blocked=red, active=urgency ramp imported from task-card); borderColor overrides per item; overdue adds a red end-cap.
  • Tooltip: lightweight by default; pass renderTooltip={(item) => <GanttFullCardTooltip item={item} />} to lazy-embed the full card (task-card only enters the bundle then).
  • Keyboard: the gutter is a WAI-ARIA tree — ↑/↓ move rows, ←/→ collapse/expand, Enter activates, Space toggles. Pan/zoom is gesture + toolbar.
  • SSR: pass now for deterministic first paint; otherwise the today line + urgency resolve after mount.

Features

  • One bar per TaskItem: effectiveStart = startAt ?? setAt; effectiveEnd = expireAt ?? (start + duration); no end ⇒ milestone diamond
  • Collapsible WBS summary rows from children; summary bar spans min(child start) → max(child end)
  • Continuous zoom (pixels-per-time) with five named header buckets (hour · day · week · month · quarter) auto-selected with hysteresis; default week
  • Pan/swipe/zoom canvas: drag-pan with dominant-direction lock + flick momentum + boundary resistance; pinch + ⌘/ctrl-wheel focal-point zoom; +/−/fit toolbar; disableGestures opt-out
  • Filled bars colored by status tone: done=gray, blocked=red, active=time-urgency ramp (green→red) imported from task-card; per-item borderColor override; overdue red end-cap
  • Today 'now' line; SSR-safe first paint (now prop seeds; otherwise deferred to post-mount); colorRefreshIntervalMs tick
  • Frozen gutter tree: caret + assignee avatar + name + label dots + status badge; row virtualization via @tanstack/react-virtual (gutter mirrors the body scroll)
  • Read-only interactions: lightweight hover tooltip (renderTooltip override; GanttFullCardTooltip lazy-embeds the full card), click select + onTaskClick, collapse/expand, onViewportChange
  • WAI-ARIA tree on the gutter: role=tree/treeitem + aria-level/expanded/selected; arrow nav + Home/End + Enter + Space (conflict-free with viewport pan)
  • Imperative handle: scrollToDate / scrollToItem / scrollToToday / expandAll / collapseAll / setZoom / zoomBy / zoomToFit / addTask / deleteTask / editTask / beginRename
  • States: empty / loading (GanttTimelineSkeleton) / single / deep-nest / all-milestones / dense; weekend shading (opt-in, day/week zoom)
  • v0.2 editing (opt-in via `editable`, default off = byte-identical v1): drag-to-reschedule + edge-resize (snap to active unit, Alt = free) + milestone drag, with a live preview ghost
  • v0.2 CRUD: draw-on-canvas + gutter + create, delete (affordance / Delete key / context-menu), inline rename (double-click / F2), gutter reparent+reorder via @dnd-kit three-zone drops
  • v0.2 detail editing: right-click context-menu (Edit / Add / Status / Delete) + embedded <TaskCard editable> in a lazy overlay; edits fire onItemAdded/Removed/Moved/onFieldEdited/onStatusChanged + onChange(TaskItem[])
  • v0.2 permissions: reuses task-card's TaskPermissions matrix (default/byLevel/byItem/inherit) + canMove/Resize/Delete/CreateChild/EditItem predicates + onPermissionDenied; item.locked blocks all; summary bars non-manipulable for single-edit (group-move added in v0.3)
  • v0.3 group-move: drag a WBS summary bracket → rigidly shift the whole subtree by one snapped delta (Alt = free); atomic permission (summary + every descendant leaf movable, else the drag pans); fires per-leaf onFieldEdited/onTaskReschedule + one onChange; imperative shiftTaskGroup(id, deltaMs)
  • v0.4 Draw mode: a toolbar Draw toggle (editable only) gates draw-to-create — empty-row drag pans by default (off) and draws a new task only when on, ending the draw-vs-pan conflict; the whole gesture pipeline defers intent to the first move (a press classifies what's under it, then click→select, vertical→scroll, horizontal→resize/move/group-move/draw/pan); right-click menu fix — its portaled pointer/key events no longer bubble back into the canvas and swallow menu-item clicks
  • v0.5 double-click-to-create: double-click an empty row area → a lightweight quick-composer floats at the pointer (name autofocus + status pick, Enter creates a snapped sibling, More options → full editor); independent of Draw mode; `quickCompose` (default true) / `renderQuickComposer` override
  • v0.5 cross-surface clipboard: copy/cut/paste TaskItems through the shared `ilinxa/task` envelope (task-card/lib/clipboard) — ⌘/Ctrl+C·X·V (document-level, gated on focus + skipped over inputs) + right-click Copy/Cut; paste re-ids each subtree and lands as a sibling of the selection (dates preserved); interops with calendar / card-tree / tree
  • v0.5 Priority submenu: the right-click menu gains a Priority picker (when priorityOptions provided); changePriority mutates + onChange only (parity with calendar — 'priority' is not a typed field event)

Tags

gantt-timelinegantttimelinescheduleroadmapprojectwbstodo

Dependencies

shadcn primitives: avatar, badge, button, context-menu, input, separator, skeleton
npm peer deps: lucide-react@^1.11.0, @tanstack/react-virtual@^3.13.24, @dnd-kit/core@^6.3.1
internal: task-card