Custom Shadcn Gantt for React and Tailwind CSS. A headless-first gantt with split tree and timeline panes, day to year scales, zoom, drag and resize scheduling, progress, summary rollups, and an external CRUD contract.
The gantt package ships a subscribable headless engine (useGanttState) and a composable view layer on top of it: a resizable tree pane for the resource hierarchy, a horizontal timeline with day, week, month, quarter, and year scales, zoom, infinite scrolling, drag and resize scheduling with live validation, per-bar progress fills, and duration-weighted summary rollups on parent rows. The engine never mutates your data on its own; every timing change flows through one proposal funnel (onEventUpdate, canDropEvent) so external CRUD stays in your hands.
The composition contract is <Gantt><GanttNav /><GanttView /></Gantt>. The root provides the calendar instance and the view configuration through context; the nav family, the view, and GanttBar all read from it, so any piece can be replaced with your own markup driven by the same hooks.
Installation
pnpm dlx shadcn@latest add @reui/gantt
Usage
import { Gantt } from "@/components/reui/gantt/gantt"import { GanttNav, GanttToolbar } from "@/components/reui/gantt/gantt-nav"import type { GanttEvent, GanttResource,} from "@/components/reui/gantt/gantt-types"import { GanttView } from "@/components/reui/gantt/gantt-view"
Give the root an explicit height (className="h-[480px]" or a flex parent): the root is a min-height-zero flex column and the view fills whatever it gets. The root also owns the type scale: every gantt label inherits its text size (text-xs by default), so className="text-sm" scales the whole component up in one place. Events work controlled (events + onEventsChange) or uncontrolled (defaultEvents); the same pairs exist for scale, date, selection, and interactions. Every drag, resize, and API timing change is proposed through onEventUpdate before it commits, canDropEvent validates live during the gesture, and a false return reverts the bar with no cleanup on your side, which makes persisting to a backend a matter of handling one callback.
The demo at the top of this page is the full composition: its settings menu drives every view configuration and interaction flag as controlled props from consumer state, and the unscheduled Launch tasks show the slot-selection contract - hover an empty row and click the hint tile (or drag a range) to schedule it through onSelectSlot. For the full set of variations, browse the Gantt components.
For AI Agents
Hand your agent this brief or the ReUI MCP, not a screenshot. Free to install.
When to use
Project plans where a tree of phases and tasks owns bars dragged and resized along a time axis
Roadmaps, status reports and team capacity views read anywhere from a single day to a whole year
Delivery tracked against a frozen plan, or work chained by dependency arrows and milestone diamonds
When not to use
A finished planner page with an edit dialog, dispatch rows and rules checked on drop: use the Gantt blocks
A quarter of sales deals grouped by account, beside board, table and calendar views: use the CRM blocks
A list of dated steps or updates read in order, with no bars to drag or resize: use the Timeline component
Agent notes
Replace buildBars, which dates its sample bars from the start of the current week, with your own records
Every example seeds bars through defaultEvents alone; pass events with onEventsChange to keep edits in your store
"use client"import { useMemo, useRef, useState } from "react"import { Gantt, type GanttApi,} from "@/components/reui/gantt/gantt"
import {
GanttNav,
GanttToolbar,
} from "@/components/reui/gantt/gantt-nav"
import type {
GanttEvent,
GanttResource,
} from "@/components/reui/gantt/gantt-types"
import { GanttView } from "@/components/reui/gantt/gantt-view"
import { addDays, startOfDay, startOfWeek } from "date-fns"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { PlusIcon } from 'lucide-react'
/**
* Yearly roadmap: each workstream is a swimlane group and its multi-month
* initiatives are the bars. The Backlog group ships with empty rows - the
* toolbar button schedules the next one onto the timeline.
*/
const RESOURCES: GanttResource[] = [
{
id: "platform",
title: "Platform",
children: [
{ id: "auth-revamp", title: "Auth Revamp" },
{ id: "api-v2", title: "API v2" },
],
},
{
id: "growth",
title: "Growth",
children: [
{ id: "onboarding", title: "Onboarding Flow" },
{ id: "referrals", title: "Referral Program" },
],
},
{
id: "design-system",
title: "Design System",
children: [
{ id: "tokens", title: "Design Tokens" },
{ id: "components", title: "Component Library" },
],
},
{
id: "backlog",
title: "Backlog",
children: [
{ id: "search", title: "Search Revamp" },
{ id: "billing", title: "Billing v2" },
{ id: "mobile", title: "Mobile App" },
],
},
]
/** Backlog initiatives, scheduled one per click in this order. */
const BACKLOG: Array<{ id: string; title: string; color: string }> = [
{ id: "search", title: "Search Revamp", color: "var(--color-rose-500)" },
{ id: "billing", title: "Billing v2", color: "var(--color-amber-500)" },
{ id: "mobile", title: "Mobile App", color: "var(--color-cyan-500)" },
]
/** Roadmap fixture - initiatives span months so the quarter axis has something
* to show, and offsets straddle today so both past and future are visible. */
function buildBars(anchor: Date): GanttEvent[] {
const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
const day = (dayOffset: number) => addDays(week, dayOffset)
const bar = (
resourceId: string,
title: string,
startOffset: number,
days: number,
color: string,
progress?: number
): GanttEvent => ({
id: `bar-${resourceId}`,
title,
start: day(startOffset),
end: day(startOffset + days),
allDay: true,
color,
resourceId,
progress,
})
return [
bar("auth-revamp", "Auth Revamp", -30, 60, "var(--color-blue-500)", 100),
bar("api-v2", "API v2", 20, 90, "var(--color-sky-500)", 30),
bar(
"onboarding",
"Onboarding Flow",
-20,
60,
"var(--color-emerald-500)",
80
),
bar("referrals", "Referral Program", 50, 90, "var(--color-teal-500)", 0),
bar("tokens", "Design Tokens", -60, 70, "var(--color-violet-500)", 100),
bar(
"components",
"Component Library",
0,
120,
"var(--color-purple-500)",
45
),
]
}
export function Pattern() {
const bars = useMemo(() => buildBars(new Date()), [])
const apiRef = useRef<GanttApi | null>(null)
// How many backlog initiatives have been scheduled so far.
const [scheduled, setScheduled] = useState(0)
// Schedule the next unscheduled backlog initiative onto its (empty) row.
// The "next" one is derived from the live events, not a captured counter,
// so it stays correct even if the button is clicked in quick succession.
const addInitiative = () => {
const api = apiRef.current
if (!api) return
const scheduledIds = new Set(api.getEvents().map((event) => event.id))
const index = BACKLOG.findIndex(
(item) => !scheduledIds.has(`bar-${item.id}`)
)
if (index === -1) return
const item = BACKLOG[index]
const week = startOfWeek(startOfDay(new Date()), { weekStartsOn: 0 })
// Land the scheduled bars in the near-future part of the current quarter so
// each one is visible the moment it drops onto its row.
const start = addDays(week, 12 + index * 20)
api.addEvent({
id: `bar-${item.id}`,
title: item.title,
start,
end: addDays(start, 24),
allDay: true,
color: item.color,
resourceId: item.id,
})
setScheduled((count) => count + 1)
}
return (
<div className="w-full p-4">
<Card className="w-full py-0">
<CardContent className="p-0">
<Gantt
defaultEvents={bars}
resources={RESOURCES}
defaultScale="quarter"
apiRef={apiRef}
treePanel={{ width: 200 }}
className="h-[480px] w-full"
>
<div className="flex flex-wrap items-center gap-2 border-b pe-3">
<GanttNav className="min-w-0 flex-1 border-b-0" />
<GanttToolbar>
<Button
variant="outline"
size="sm"
onClick={addInitiative}
disabled={scheduled >= BACKLOG.length}
>
<PlusIcon className="size-4" aria-hidden="true" />
Add to roadmap
</Button>
</GanttToolbar>
</div>
{/* Parent workstream rows carry no bars - summaryBars rolls up the
child initiatives into one envelope on the group row. */}
<GanttView />
</Gantt>
</CardContent>
</Card>
</div>
)
}
A long-horizon plan on the quarter scale. Workstreams are swimlane groups whose multi-month initiatives roll up into automatic summary bars, so leadership reads the whole year at a glance and navigates a quarter at a time. The toolbar button schedules the next backlog initiative onto its empty row through the addEvent API.
Team Capacity Schedule
"use client"import { useMemo, useRef } from "react"import { Gantt, type GanttApi,} from "@/components/reui/gantt/gantt"
import {
GanttNav,
GanttToolbar,
} from "@/components/reui/gantt/gantt-nav"
import type {
GanttEvent,
GanttResource,
} from "@/components/reui/gantt/gantt-types"
import { GanttView } from "@/components/reui/gantt/gantt-view"
import { addDays, startOfDay, startOfWeek } from "date-fns"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { PlusIcon } from 'lucide-react'
/**
* People-centric capacity board: rows are teammates grouped by squad, and
* each teammate's bars are the week's assignments. The toolbar drops a fresh
* assignment onto the next teammate, so a person can hold several at once.
*/
const RESOURCES: GanttResource[] = [
{
id: "product-squad",
title: "Product Squad",
children: [
{ id: "ada", title: "Ada Lovelace" },
{ id: "alan", title: "Alan Turing" },
],
},
{
id: "design-squad",
title: "Design Squad",
children: [
{ id: "grace", title: "Grace Hopper" },
{ id: "linus", title: "Linus Torvalds" },
],
},
]
/** Avatar photo + initials keyed by person id - GanttResource carries no
* custom fields, so per-row presentation data lives in a lookup of your own.
* The initials show while the image loads or if it fails. */
const resourceMeta: Record<string, { initials: string; avatar: string }> = {
ada: {
initials: "AL",
avatar: "https://randomuser.me/api/portraits/women/44.jpg",
},
alan: {
initials: "AT",
avatar: "https://randomuser.me/api/portraits/men/32.jpg",
},
grace: {
initials: "GH",
avatar: "https://randomuser.me/api/portraits/women/68.jpg",
},
linus: {
initials: "LT",
avatar: "https://randomuser.me/api/portraits/men/54.jpg",
},
}
/** Teammates the new assignments cycle through, and a small pool to name and
* color them from. */
const PEOPLE = ["ada", "alan", "grace", "linus"]
const TASK_POOL = [
{ title: "Bug triage", color: "var(--color-amber-500)" },
{ title: "Code review", color: "var(--color-rose-500)" },
{ title: "Spec draft", color: "var(--color-teal-500)" },
{ title: "Pairing", color: "var(--color-indigo-500)" },
]
/** This-week assignments per teammate - day-precise bars for a capacity read
* (progress omitted; occupancy, not percent-done, is the point here). */
function buildBars(anchor: Date): GanttEvent[] {
const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
const day = (dayOffset: number) => addDays(week, dayOffset)
const bar = (
resourceId: string,
title: string,
startOffset: number,
days: number,
color: string
): GanttEvent => ({
id: `bar-${resourceId}`,
title,
start: day(startOffset),
end: day(startOffset + days),
allDay: true,
color,
resourceId,
})
return [
bar("ada", "Checkout API", -1, 3, "var(--color-blue-500)"),
bar("alan", "Search Indexing", 0, 3, "var(--color-sky-500)"),
bar("grace", "Dashboard Redesign", 1, 3, "var(--color-violet-500)"),
bar("linus", "Infra Migration", -2, 3, "var(--color-purple-500)"),
]
}
export function Pattern() {
const bars = useMemo(() => buildBars(new Date()), [])
const apiRef = useRef<GanttApi | null>(null)
// A ref (not state) counts the adds, so it advances synchronously and stays
// correct even when the button is clicked several times in a row.
const addedRef = useRef(0)
// Add a 2-day assignment to the next teammate in rotation. Successive adds
// to the same person overlap and stack into extra lanes on that row.
const addAssignment = () => {
const api = apiRef.current
if (!api) return
const n = addedRef.current++
const person = PEOPLE[n % PEOPLE.length]
const task = TASK_POOL[n % TASK_POOL.length]
const week = startOfWeek(startOfDay(new Date()), { weekStartsOn: 0 })
const start = addDays(week, (n % 5) + 1)
api.addEvent({
id: `bar-extra-${n}`,
title: task.title,
start,
end: addDays(start, 2),
allDay: true,
color: task.color,
resourceId: person,
})
}
return (
<div className="w-full p-4">
<Card className="w-full py-0">
<CardContent className="p-0">
<Gantt
defaultEvents={bars}
resources={RESOURCES}
defaultScale="week"
apiRef={apiRef}
// Weekends shaded so booked working-day capacity is obvious.
offDays={true}
treePanel={{ width: 220 }}
// People rows get an avatar label; group (squad) rows return
// undefined to keep the default plain-title label.
renderResourceLabel={({ resource, isGroup }) => {
if (isGroup) return undefined
const person = resourceMeta[resource.id]
return (
<span className="flex min-w-0 items-center gap-2">
<Avatar className="size-5">
<AvatarImage src={person?.avatar} alt={resource.title} />
<AvatarFallback className="text-[10px]">
{person?.initials ?? resource.title.charAt(0)}
</AvatarFallback>
</Avatar>
<span className="truncate">{resource.title}</span>
</span>
)
}}
className="h-[440px] w-full"
>
<div className="flex flex-wrap items-center gap-2 border-b pe-3">
<GanttNav className="min-w-0 flex-1 border-b-0" />
<GanttToolbar>
<Button variant="outline" size="sm" onClick={addAssignment}>
<PlusIcon className="size-4" aria-hidden="true" />
Add assignment
</Button>
</GanttToolbar>
</div>
<GanttView />
</Gantt>
</CardContent>
</Card>
</div>
)
}
A people-centric weekly view. Each row is a teammate rendered with an avatar label via renderResourceLabel, their bars are the week's assignments, and weekends are shaded with offDays so real working-day capacity is obvious. The toolbar drops a fresh assignment onto the next teammate, so one person can hold several bars at once.
Project Status Report
"use client"import { useMemo, useRef, useState } from "react"import { Badge, type BadgeProps } from "@/components/reui/badge"import { Gantt, type GanttApi,
type GanttColumn,
} from "@/components/reui/gantt/gantt"
import {
GanttNav,
GanttToolbar,
} from "@/components/reui/gantt/gantt-nav"
import type {
GanttEvent,
GanttResource,
} from "@/components/reui/gantt/gantt-types"
import { GanttView } from "@/components/reui/gantt/gantt-view"
import { addDays, startOfDay, startOfWeek } from "date-fns"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { PlusIcon } from 'lucide-react'
type TaskMeta = { owner: string; status: string }
/** Owner headshots keyed by name. Missing entries (e.g. "Unassigned") fall
* back to initials, and the initials also show while the photo loads. */
const OWNER_AVATARS: Record<string, string> = {
"Ada Lovelace": "https://randomuser.me/api/portraits/women/44.jpg",
"Grace Hopper": "https://randomuser.me/api/portraits/women/68.jpg",
"Alan Turing": "https://randomuser.me/api/portraits/men/32.jpg",
"Linus Torvalds": "https://randomuser.me/api/portraits/men/54.jpg",
"Katherine Johnson": "https://randomuser.me/api/portraits/women/90.jpg",
"Margaret Hamilton": "https://randomuser.me/api/portraits/women/12.jpg",
}
/** First + last initial from an owner name, used as the avatar fallback -
* "Ada Lovelace" reads "AL", "Unassigned" reads "UN". */
function ownerInitials(name: string) {
const parts = name.trim().split(/\s+/).filter(Boolean)
if (parts.length === 0) return "?"
if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase()
return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase()
}
/** Status label -> ReUI badge light variant (green done, amber in progress,
* neutral not started). */
const STATUS_VARIANT: Record<string, BadgeProps["variant"]> = {
Done: "success-light",
"In progress": "warning-light",
"Not started": "secondary",
}
/**
* Status report: the left tree panel doubles as a task table with Owner and
* Status columns beside each phase, every bar carries a progress fill, and the
* timeline is drag-locked. New tasks are appended as rows from the toolbar.
*/
const INITIAL_RESOURCES: GanttResource[] = [
{
id: "planning",
title: "Planning",
children: [
{ id: "requirements", title: "Requirements" },
{ id: "design-phase", title: "Design" },
],
},
{
id: "build",
title: "Build",
children: [
{ id: "frontend", title: "Frontend" },
{ id: "backend", title: "Backend" },
],
},
{
id: "launch",
title: "Launch",
children: [
{ id: "qa", title: "QA & Testing" },
{ id: "rollout", title: "Rollout" },
],
},
]
/** Owner + status per task, keyed by resource.id - GanttResource has no room
* for custom fields, so the extra column data lives in a lookup of your own. */
const INITIAL_META: Record<string, TaskMeta> = {
requirements: { owner: "Ada Lovelace", status: "Done" },
"design-phase": { owner: "Grace Hopper", status: "Done" },
frontend: { owner: "Alan Turing", status: "In progress" },
backend: { owner: "Linus Torvalds", status: "In progress" },
qa: { owner: "Katherine Johnson", status: "Not started" },
rollout: { owner: "Margaret Hamilton", status: "Not started" },
}
/** Status-report fixture - progress descends from finished planning to
* not-started launch, and the phases straddle today so the now-line falls
* mid-plan. Kept inside a month so every phase reads at a glance. */
function buildBars(anchor: Date): GanttEvent[] {
const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
const day = (dayOffset: number) => addDays(week, dayOffset)
const bar = (
resourceId: string,
title: string,
startOffset: number,
days: number,
color: string,
progress?: number
): GanttEvent => ({
id: `bar-${resourceId}`,
title,
start: day(startOffset),
end: day(startOffset + days),
allDay: true,
color,
resourceId,
progress,
})
return [
bar("requirements", "Requirements", -10, 8, "var(--color-blue-500)", 100),
bar("design-phase", "Design", -8, 8, "var(--color-sky-500)", 100),
bar("frontend", "Frontend", -2, 10, "var(--color-violet-500)", 60),
bar("backend", "Backend", 0, 10, "var(--color-purple-500)", 45),
bar("qa", "QA & Testing", 8, 8, "var(--color-amber-500)", 0),
bar("rollout", "Rollout", 14, 5, "var(--color-emerald-500)", 0),
]
}
export function Pattern() {
const bars = useMemo(() => buildBars(new Date()), [])
const apiRef = useRef<GanttApi | null>(null)
const [resources, setResources] = useState<GanttResource[]>(INITIAL_RESOURCES)
const [meta, setMeta] = useState<Record<string, TaskMeta>>(INITIAL_META)
// A ref (not state) numbers each appended task, so ids stay unique even when
// the button is clicked several times before a re-render.
const addedRef = useRef(0)
// Extra tree-panel columns after the pinned name column. Built from meta so
// appended rows pick up their Owner and Status; group rows return null.
const columns = useMemo<GanttColumn[]>(
() => [
{
id: "owner",
title: "Owner",
width: 130,
align: "start",
render: ({ resource, isGroup }) => {
if (isGroup) return null
const owner = meta[resource.id]?.owner
if (!owner) return null
return (
<span className="flex min-w-0 items-center gap-2">
<Avatar className="size-5 shrink-0">
<AvatarImage src={OWNER_AVATARS[owner]} alt={owner} />
<AvatarFallback className="text-[10px]">
{ownerInitials(owner)}
</AvatarFallback>
</Avatar>
<span className="truncate">{owner}</span>
</span>
)
},
},
{
id: "status",
title: "Status",
width: 100,
align: "start",
render: ({ resource, isGroup }) => {
if (isGroup) return null
const status = meta[resource.id]?.status
return status ? (
<Badge variant={STATUS_VARIANT[status] ?? "secondary"} size="sm">
{status}
</Badge>
) : null
},
},
],
[meta]
)
// Append a new task as its own row under the Launch phase, register its
// Owner/Status, and drop a not-started bar on its row.
const addTask = () => {
const api = apiRef.current
if (!api) return
const n = ++addedRef.current
const id = `task-${n}`
setResources((prev) =>
prev.map((group) =>
group.id === "launch"
? {
...group,
children: [
...(group.children ?? []),
{ id, title: `New task ${n}` },
],
}
: group
)
)
setMeta((prev) => ({
...prev,
[id]: { owner: "Unassigned", status: "Not started" },
}))
const week = startOfWeek(startOfDay(new Date()), { weekStartsOn: 0 })
const start = addDays(week, (n % 6) - 2)
api.addEvent({
id: `bar-${id}`,
title: `New task ${n}`,
start,
end: addDays(start, 5),
allDay: true,
color: "var(--color-slate-400)",
resourceId: id,
progress: 0,
})
}
return (
<div className="w-full p-4">
<Card className="w-full py-0">
<CardContent className="p-0">
<Gantt
defaultEvents={bars}
resources={resources}
defaultScale="month"
apiRef={apiRef}
// Read-only timeline: drag, resize and slot-select are off so the
// plan can't be shifted by dragging; rows are added via the toolbar.
defaultInteractions={{
drag: false,
resize: false,
selectSlot: false,
}}
columns={columns}
// Wider tree with a tighter name column so the Owner avatar,
// owner name and Status all fit alongside the task names.
treePanel={{ width: 400, nameColumnWidth: 150 }}
className="h-[500px] w-full"
>
<div className="flex flex-wrap items-center gap-2 border-b pe-3">
<GanttNav className="min-w-0 flex-1 border-b-0" />
<GanttToolbar>
<Button variant="outline" size="sm" onClick={addTask}>
<PlusIcon className="size-4" aria-hidden="true" />
Add task
</Button>
</GanttToolbar>
</div>
<GanttView />
</Gantt>
</CardContent>
</Card>
</div>
)
}
A report on the month scale. Owner and Status columns sit beside each phase in the tree panel via the columns prop and every bar carries a progress fill. Drag, resize, and slot-select are disabled so the plan can't be shifted by dragging, while the toolbar appends new tasks as their own rows with controlled resources.
As-Built Baseline Tracking
"use client"import { useMemo } from "react"import { Gantt } from "@/components/reui/gantt/gantt"import { GanttNav } from "@/components/reui/gantt/gantt-nav"import type { GanttEvent,
GanttResource,
} from "@/components/reui/gantt/gantt-types"
import { GanttView } from "@/components/reui/gantt/gantt-view"
import { addDays, startOfDay, startOfWeek } from "date-fns"
import { Card, CardContent } from "@/components/ui/card"
/** Two-level white-label delivery tree; bars attach to leaf rows by id. A
* NODE can carry its own plan too: the Implementation group ghosts a band
* for the window the whole phase was booked into. */
function buildResources(anchor: Date): GanttResource[] {
const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
const day = (dayOffset: number) => addDays(week, dayOffset)
return [
{
id: "discovery",
title: "Discovery",
children: [
{ id: "kickoff", title: "Kickoff workshop" },
{ id: "requirements", title: "Requirements" },
],
},
{
id: "implementation",
title: "Implementation",
baselineStart: day(-3),
baselineEnd: day(6),
children: [
{ id: "integration", title: "API integration" },
{ id: "migration", title: "Data migration" },
{ id: "hardening", title: "Security hardening" },
],
},
{
id: "launch",
title: "Launch",
children: [{ id: "handover", title: "Handover" }],
},
]
}
/** Leaf titles for bar labels, resolved from the tree by id. */
function leafTitles(resources: GanttResource[]): Map<string, string> {
return new Map(
resources
.flatMap((group) => group.children ?? [])
.map((leaf) => [leaf.id, leaf.title])
)
}
/** As-built fixture around the current week: frozen plan vs actual dates. */
function buildBars(anchor: Date, titles: Map<string, string>): GanttEvent[] {
const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
const day = (dayOffset: number) => addDays(week, dayOffset)
// The baseline pair is the plan as it was frozen; start/end is what really
// happened. Equal baseline instants (plannedDays 0) mark a planned
// milestone, drawn as a diamond instead of a ghost bar.
const bar = (
resourceId: string,
startOffset: number,
days: number,
plannedOffset: number,
plannedDays: number,
color: string,
progress?: number
): GanttEvent => ({
id: `bar-${resourceId}`,
title: titles.get(resourceId) ?? resourceId,
start: day(startOffset),
end: day(startOffset + days),
baselineStart: day(plannedOffset),
baselineEnd: day(plannedOffset + plannedDays),
allDay: true,
color,
resourceId,
progress,
})
return [
// delivered exactly to plan
bar("kickoff", -9, 2, -9, 2, "var(--color-blue-500)", 100),
// ran a day over its planned window
bar("requirements", -7, 4, -7, 3, "var(--color-sky-500)", 100),
// started a day late and carried the slip through
bar("integration", -2, 5, -3, 5, "var(--color-violet-500)", 60),
// tracking a day ahead of plan
bar("migration", 1, 3, 1, 4, "var(--color-emerald-500)", 30),
// slipped on both ends
bar("hardening", 4, 4, 3, 3, "var(--color-teal-500)", 0),
// planned milestone (diamond) vs the day the handover actually took
bar("handover", 9, 1, 8, 0, "var(--color-amber-500)"),
]
}
export function Pattern() {
const resources = useMemo(() => buildResources(new Date()), [])
const bars = useMemo(
() => buildBars(new Date(), leafTitles(resources)),
[resources]
)
return (
<div className="w-full p-4">
<Card className="w-full py-0">
<CardContent className="p-0">
{/* baselineBars is on by default: any event carrying baselineStart
and baselineEnd ghosts its planned window behind the bar, and
dragging a bar re-times the actual dates while the plan holds
still. Bars expose data-baseline-variance="early|late|on-time"
for consumer styling, and the tooltip names the planned range. */}
<Gantt
defaultEvents={bars}
resources={resources}
defaultScale="month"
scheduleMode="single"
treePanel={{ width: 240 }}
className="h-[480px] w-full"
>
<GanttNav />
<GanttView />
</Gantt>
</CardContent>
</Card>
</div>
)
}
Planned versus actual. Every bar carries baselineStart and baselineEnd, so the frozen plan ghosts behind the actual dates on the same lane, and equal baseline instants render a planned milestone diamond. Dragging a bar re-times the actual dates while the ghost holds still; the tooltip names the planned range and each bar exposes data-baseline-variance="early|late|on-time" for consumer styling. The Implementation group also carries a NODE-level plan, drawn as a faint band behind the whole row (GanttResource.baselineStart/baselineEnd). Turn the ghosts off with baselineBars={false} or restyle them via classNames.baseline, renderBaseline, and renderRowBaseline.
Dependencies and Milestones
"use client"import { useMemo } from "react"import { Gantt } from "@/components/reui/gantt/gantt"import { GanttNav } from "@/components/reui/gantt/gantt-nav"import type { GanttEvent,
GanttResource,
} from "@/components/reui/gantt/gantt-types"
import { GanttView } from "@/components/reui/gantt/gantt-view"
import { addDays, startOfDay, startOfWeek } from "date-fns"
import { Card, CardContent } from "@/components/ui/card"
/** Two-level white-label rollout tree; bars attach to leaf rows by id. */
const RESOURCES: GanttResource[] = [
{
id: "build",
title: "Build",
children: [
{ id: "kickoff", title: "Kickoff" },
{ id: "api", title: "API work" },
{ id: "frontend", title: "Frontend" },
{ id: "qa", title: "QA pass" },
],
},
{
id: "rollout",
title: "Rollout",
children: [
{ id: "staging", title: "Staging deploy" },
{ id: "signoff", title: "Sign-off" },
{ id: "live", title: "Go live" },
],
},
]
/** Leaf titles for bar labels, resolved from the tree by id. */
const RESOURCE_TITLES = new Map(
RESOURCES.flatMap((group) => group.children ?? []).map((leaf) => [
leaf.id,
leaf.title,
])
)
/** Chained fixture around the current week: FS arrows plus a milestone gate. */
function buildBars(anchor: Date): GanttEvent[] {
const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
const day = (dayOffset: number) => addDays(week, dayOffset)
// `dependencies` names PREDECESSOR ids: each arrow runs from that bar's
// end into this one's start. A zero-length event (days 0) is a MILESTONE
// and renders as a diamond instead of a bar.
const bar = (
resourceId: string,
startOffset: number,
days: number,
color: string,
dependencies?: string[],
progress?: number
): GanttEvent => ({
id: `bar-${resourceId}`,
title: RESOURCE_TITLES.get(resourceId) ?? resourceId,
start: day(startOffset),
end: day(startOffset + days),
allDay: true,
color,
resourceId,
dependencies,
progress,
})
return [
bar("kickoff", -9, 2, "var(--color-blue-500)", undefined, 100),
bar("api", -7, 5, "var(--color-sky-500)", ["bar-kickoff"], 80),
bar("frontend", -4, 5, "var(--color-violet-500)", ["bar-kickoff"], 55),
// waits on BOTH build tracks, so two arrows land on its start
bar("qa", 2, 3, "var(--color-purple-500)", ["bar-api", "bar-frontend"], 10),
bar("staging", 5, 2, "var(--color-emerald-500)", ["bar-qa"]),
// the gate: a diamond with an arrow in from staging and one out to launch
bar("signoff", 8, 0, "var(--color-amber-500)", ["bar-staging"]),
bar("live", 9, 1, "var(--color-teal-500)", ["bar-signoff"]),
]
}
export function Pattern() {
const bars = useMemo(() => buildBars(new Date()), [])
return (
<div className="w-full p-4">
<Card className="w-full py-0">
<CardContent className="p-0">
{/* dependencyLines is on by default: any event naming
`dependencies` gets a finish-to-start arrow from each
predecessor, drawn under the bars and skipped when either end
is on a hidden row. Dragging a bar re-anchors its arrows on
release; the milestone diamond moves like any bar but has no
resize edges. */}
<Gantt
defaultEvents={bars}
resources={RESOURCES}
defaultScale="month"
scheduleMode="single"
treePanel={{ width: 240 }}
className="h-[480px] w-full"
>
<GanttNav />
<GanttView />
</Gantt>
</CardContent>
</Card>
</div>
)
}
A chained rollout plan. Each event's dependencies array names its predecessors, and the view draws a finish-to-start elbow arrow from every predecessor's end into the dependent's start, under the bars. The zero-duration "Sign-off" event renders as a milestone diamond: it moves like any bar (the move keeps it a point) but has no resize edges, its title always sits beside it, and screen readers hear the milestone clause. Turn the arrows off with dependencyLines={false} or restyle the layer via classNames.dependencies.
API Reference
Gantt
The root provider and container. It creates (or adopts) the calendar instance, provides it through context, and renders a div shell with an aria-live announcer. Besides the props below, it accepts every state option (see State options), every callback (see Callbacks and validators), and every view configuration key (see View configuration) as flat props.
Prop
Type
Default
Description
calendar
GanttInstance<TData>
-
Adopt a hoisted useGanttState instance; option props are then ignored.
apiRef
RefObject<GanttApi<TData> | null>
-
Imperative escape hatch usable from outside the tree.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes for the root container.
children
ReactNode
-
The gantt composition (GanttNav, GanttToolbar, GanttView).
GanttNav
The composed navigation bar: Today, scale switcher, prev/next, and the period title with a trailing spacer. Pass children to use it as a pure layout shell instead. The title follows the viewport center while scrolling so the header always names what you are looking at.
Prop
Type
Default
Description
children
ReactNode
-
Custom nav content; replaces the default composition.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes.
GanttNavToday
Button that navigates to today. Renders the today i18n label by default and marks itself with data-active while the anchor period contains now.
Prop
Type
Default
Description
children
ReactNode
i18n today
Custom button content.
tooltip
ReactNode | null
the current date
Hover/focus-visible tooltip; null disables it. Never re-triggers from a pointer click.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes.
GanttNavPrev
Icon button that steps the anchor date one period back at the current scale.
Prop
Type
Default
Description
children
ReactNode
chevron icon
Custom button content.
tooltip
ReactNode | null
i18n previous
Hover/focus-visible tooltip; null disables.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes.
GanttNavNext
Icon button that steps the anchor date one period forward at the current scale.
Prop
Type
Default
Description
children
ReactNode
chevron icon
Custom button content.
tooltip
ReactNode | null
i18n next
Hover/focus-visible tooltip; null disables.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes.
GanttTitle
The current period title, formatted by i18n.functions.formatTitle and announced politely on change.
Prop
Type
Default
Description
format
(ctx: { title: string }) => ReactNode
-
Wraps or replaces the formatted title text.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes.
GanttScaleSwitcher
Dropdown that switches between the Day, Week, Month, Quarter, and Year scales. Tooltips on this overlay-opener are hover-only so nothing flashes when focus returns after the menu closes.
Prop
Type
Default
Description
scales
GanttScale[]
all five
The offered scales, in menu order.
children
ReactNode
current scale label
Custom trigger content.
tooltip
ReactNode | null
i18n selectView
Hover-only tooltip; null disables.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes.
GanttDatePicker
Compact go-to-date picker (shadcn Calendar in a popover). Not part of the default GanttNav composition; add it to a custom nav when needed. It has no tooltip by design because it opens an overlay.
Prop
Type
Default
Description
className
string
-
Additional CSS classes.
GanttToolbar
Free slot for consumer toolbar buttons; a pure layout shell that also picks up classNames.toolbar from the view configuration.
Prop
Type
Default
Description
children
ReactNode
-
Toolbar content.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes.
GanttView
The gantt body: split resizable tree and timeline panes with synced scrolling, the grouped two-row header, lanes, bars, summary rollups, off-screen chips, the zoom control, and all pointer interactions. Display behavior comes from the view configuration on the root.
Prop
Type
Default
Description
interval
number
interval config
Day-scale unit interval in minutes; clamped between 15 and 240.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes.
GanttBar
The one interactive bar element, rendered by the view for every visible segment. The wrapper owns positioning hooks, a11y, selection, drag and resize listeners, the range tooltip, the optional right-click menu, and data attributes (data-selected, data-dragging, data-progress, data-completed, data-past, data-recurring); content comes from children, the root renderEvent override, or the built-in default. Exported for fully custom view compositions.
Prop
Type
Default
Description
segment
GanttSegment<TData>
-
Required. The timeline segment this bar renders.
children
ReactNode
-
Replaces the default bar content; the interactive wrapper stays gantt-owned.
labelOutside
boolean
-
The title renders beside the bar (view-owned), so the default inner content is suppressed.
rowTitle
string
-
The owning row's title for the aria-label; omitting falls back to a memoized tree lookup.
asChild
boolean
false
Render the child element instead (Radix Slot).
className
string
-
Additional CSS classes.
GanttEvent
One schedulable bar. TData is a fully generic consumer payload.
Property
Type
Default
Description
id
string
-
Required. Stable event id.
title
string
-
Required. Bar title.
start
Date
-
Required. Plain instant; consumers parse ISO strings themselves.
end
Date
-
Required. Exclusive; must be greater than or equal to start. Equal to start renders the event as a milestone diamond.
allDay
boolean
-
Whether the event is date-based rather than timed.
recurrence
GanttRecurrenceRule | string
-
Structured rule or a raw "RRULE:..." line.
recurringEventId
string
-
This event is an edited single occurrence of that series.
originalStart
Date
-
Which occurrence it replaces (RECURRENCE-ID semantics).
color
string
-
Token or CSS color; flows to the --gantt-event-color CSS variable.
readOnly
boolean
-
Excluded from drag and resize regardless of interactions state.
draggable
boolean
-
Per-event override; default comes from interactions.drag.
resizable
boolean
-
Per-event override; default comes from interactions.resize.
Completion 0-100; renders as a subtle fill inside the bar.
baselineStart
Date
-
Planned (as-built baseline) start, ghosted behind the bar. Set with baselineEnd or not at all.
baselineEnd
Date
-
Planned end, exclusive like end. Equal to baselineStart marks a planned milestone diamond. Recurring events render no baseline.
zIndex
number
-
Explicit stacking override; wins over the computed z.
resourceId
string
-
Resource row this bar belongs to.
dependencies
GanttBarId[]
-
Predecessor event ids (finish-to-start); each draws an elbow arrow into this bar's start. Rendering only - the gantt never reschedules dependents.
data
TData
-
Consumer payload, fully generic.
GanttResource
A row of the gantt tree (task, person, equipment). Nesting via children renders as collapsible groups.
Property
Type
Default
Description
id
string
-
Required. Stable resource id.
title
string
-
Required. Row title.
color
string
-
Token or CSS color used for subtle row accents.
scheduleMode
"single" | "multiple"
-
Per-node cardinality override; falls back to the view-level scheduleMode.
baselineStart
Date
-
Planned window start for the WHOLE node, ghosted as a band behind its lanes. Set with baselineEnd or not at all.
baselineEnd
Date
-
Planned node window end, exclusive; equal to baselineStart marks a planned milestone.
children
GanttResource[]
-
Child rows; presence makes this row a group.
GanttOccurrence
One expanded instance of an event within the visible range (recurring events expand to many).
Property
Type
Description
key
string
Stable per instance: `${event.id}::${startISO}`.
eventId
string
The source event id.
event
GanttEvent<TData>
The source event.
start
Date
Occurrence start.
end
Date
Occurrence end (exclusive).
allDay
boolean
Whether the occurrence is date-based.
isRecurring
boolean
Whether it came from a recurrence expansion.
recurrenceIndex
number
Index within the series, when recurring.
GanttSegment
The slice of an occurrence rendered inside one timeline range, with lane packing metadata. Passed to GanttBar and every render override.
Property
Type
Description
occurrence
GanttOccurrence<TData>
The occurrence this segment slices.
day
Date
Range-start reference instant of the segment's slice.
isStart
boolean
Whether the segment contains the occurrence start.
isEnd
boolean
Whether the segment contains the occurrence end.
continuesBefore
boolean
The occurrence continues before this segment.
continuesAfter
boolean
The occurrence continues after this segment.
startMin
number
Minutes from the visible range start, clamped to the range.
endMin
number
Minutes from the visible range start, clamped to the range.
column
number
Lane assigned by overlap packing.
columnCount
number
Total lanes in the overlap cluster.
columnSpan
number
Lanes this segment may widen into.
GanttRecurrenceRule
Structured RFC 5545 subset. The built-in expander supports freq daily/weekly/monthly/yearly, interval, count, until, and weekly byWeekday without ordinals; byMonthDay, byMonth, and byWeekday outside weekly parse but throw a GanttRecurrenceError on expansion instead of silently mis-expanding. Plug the getOccurrences option for a full engine.
Property
Type
Default
Description
freq
"daily" | "weekly" | "monthly" | "yearly"
-
Required. Recurrence frequency.
interval
number
1
Period multiplier.
count
number
-
Total occurrence cap.
until
Date
-
Inclusive series end instant.
byWeekday
Array<GanttWeekday | { day: GanttWeekday; ordinal: number }>
-
Weekday filter (weekly only, no ordinals).
byMonthDay
number[]
-
Parsed but not expanded (throws on expansion).
byMonth
number[]
-
Parsed but not expanded (throws on expansion).
weekStart
GanttWeekday
-
WKST; parses and round-trips.
exDates
Date[]
-
Excluded instants; matching occurrences are removed (they still consume their count slot).
rDates
Date[]
-
Extra instants added to the series, each with the event's own duration.
Return type of onEventUpdate: false rejects and reverts; void or true accepts; { start?: Date; end?: Date; allDay?: boolean } accepts with an adjustment.
GanttSlotInfo
Payload of onSlotClick. A click is a point, not a range; end is reserved for future gestures.
Property
Type
Description
date
Date
The clicked instant.
end
Date
Reserved for future gestures.
allDay
boolean
Whether the click landed on a date-based surface.
resourceId
string
Present when the click happened inside a resource row.
GanttSlotDraft
The in-progress drag-create rectangle only, cleared on commit or cancel; the committed slot selection lives in GanttSelection.slot. Payload of onSelectSlot and canSelectSlot.
Property
Type
Description
start
Date
Draft start.
end
Date
Draft end.
allDay
boolean
Whether the draft is date-based.
resourceId
string
Present when the slot was selected inside a resource row.
Committed slot selection (drafts live in GanttSlotDraft).
GanttResourceReorder
Proposal emitted when a timeline resource row is drag-reordered. Payload of onResourceReorder, canReorderResource, and onResourceReorderReject.
Property
Type
Description
resourceId
string
The dragged resource id.
parentId
string | null
New parent id, or null for the root level.
index
number
Insertion index among the new parent's children.
resources
GanttResource[]
The full resource tree with the move applied (convenience).
GanttRangeInfo
Payload of onRangeChange; fires once on mount and whenever the rendered range changes. Fetch remote data for range.
Property
Type
Description
range
GanttDateRange
Full rendered axis range.
activeRange
GanttDateRange
The logical period (the month/week itself).
scale
GanttScale
Current scale.
date
Date
Anchor date.
timeZone
string
Display time zone.
GanttDateRange is { start: Date; end: Date } with an inclusive start and exclusive end. GanttScale is "day" \| "week" \| "month" \| "quarter" \| "year".
GanttState
The full engine snapshot, readable via instance.getState() or a useGanttSelector selector.
Property
Type
Description
scale
GanttScale
Horizontal axis scale.
date
Date
Anchor date.
visibleRange
GanttDateRange
Full rendered axis range - fetch remote data for this.
activeRange
GanttDateRange
The logical period (the month/week itself).
events
GanttEvent<TData>[]
Current events.
selection
GanttSelection
Committed selection.
interactions
GanttInteractions
Effective interaction switches.
loading
boolean
Mirrors the loading option.
drag
GanttDragState<TData> | null
In-flight drag/resize gesture, or null.
slotDraft
GanttSlotDraft | null
In-flight drag-create rectangle, or null.
viewportCenter
Date | null
Instant at the center of the scrolled viewport; the nav title follows it.
GanttDragState
The in-flight gesture stored in state.drag.
Property
Type
Description
kind
"move" | "resize-start" | "resize-end"
Gesture kind.
occurrence
GanttOccurrence<TData>
The gestured occurrence.
proposedStart
Date
Current snapped proposal start.
proposedEnd
Date
Current snapped proposal end.
proposedAllDay
boolean
Current proposal all-day flag.
proposedResourceId
string
The bar's own resource; moves are x-axis only and never cross rows.
valid
boolean
Last canDropEvent verdict; drives data-drop-invalid styling.
GanttDataAdapter
External-data contract: getEvents(range, signal?) => Promise<GanttEvent<TData>[]>. The type ships for adapter recipes (Google events.list and MS Graph calendarView map to GanttEvent in about 15 lines); OAuth, tokens, and sync loops are application backend territory.
GanttRenderEventProps
Payload of renderEvent and renderEventMenu.
Property
Type
Description
occurrence
GanttOccurrence<TData>
The bar's occurrence.
segment
GanttSegment<TData>
The rendered segment.
isDragging
boolean
Whether a gesture owns this bar.
isSelected
boolean
Whether the bar is selected.
GanttColumnContext
Row context handed to tree-panel column renderers, renderResourceLabel, renderResourceMenu, and the resource click callbacks.
Property
Type
Description
resource
GanttResource
The row's resource.
depth
number
Nesting depth (root = 0).
isGroup
boolean
Whether the row has children.
collapsed
boolean
Whether the group is currently collapsed.
GanttDragIndicatorProps
Live gesture snapshot handed to renderDragPreview and renderResizeIndicator; content re-renders per snap step while the gantt writes the wrapper position imperatively.
Property
Type
Description
occurrence
GanttOccurrence<TData>
The gestured occurrence.
kind
"move" | "resize-start" | "resize-end"
Gesture kind.
start
Date
Proposed (snapped) start of the current step.
end
Date
Proposed (snapped) end of the current step.
valid
boolean
Last canDropEvent verdict.
baseline
GanttBaseline | null
The event's planned window, so a custom overlay can keep drawing it mid-gesture.
GanttScheduleHintProps
Slot handed to a custom renderScheduleHint renderer.
Property
Type
Description
start
Date
Snapped hint start.
end
Date
Snapped hint end.
resource
GanttResource
The hovered row's resource.
GanttSummaryProps
Parent rollup handed to a custom renderSummary renderer.
Property
Type
Description
resource
GanttResource
The group row's resource.
start
Date
Envelope start of the descendant bars.
end
Date
Envelope end of the descendant bars.
progress
number | null
Duration-weighted progress, or null to hide.
GanttBaseline
A validated planned window, as returned by resolveEventBaseline.
Property
Type
Description
start
Date
Planned start.
end
Date
Planned end, exclusive.
milestone
boolean
Planned start and end coincide: a point in the plan, not a window.
GanttBaselineProps
Planned window handed to a custom renderBaseline renderer. The positioned, pointer-transparent wrapper stays gantt-owned; the renderer replaces its content, the milestone diamond included.
Property
Type
Description
event
GanttEvent<TData>
The event whose plan is being drawn.
start
Date
Planned start.
end
Date
Planned end, exclusive.
milestone
boolean
Planned start and end coincide: a point in the plan, not a window.
variance
GanttBaselineVariance
Actual end against the planned end: "early" | "late" | "on-time".
GanttRowBaselineProps
Planned NODE window handed to a custom renderRowBaseline renderer; same wrapper contract as renderBaseline.
Property
Type
Description
resource
GanttResource
The node whose plan is being drawn.
start
Date
Planned start.
end
Date
Planned end, exclusive.
milestone
boolean
Planned start and end coincide: a point in the plan, not a window.
variance
GanttBaselineVariance | null
Latest actual end (subtree included for groups) vs the planned end; null without events to compare.
GanttApi
The imperative surface, available as instance.api from useGantt/useGanttState or through the root apiRef.
Most hooks must run under a <Gantt> ancestor. The exceptions: useGanttState creates the instance itself, useGanttSelector accepts an explicit instance, useGanttSettingsVersion takes the instance as an argument, and useGanttViewConfig falls back to the default view configuration outside the tree.
Per-bar and per-row pointer gesture wiring, for custom view compositions.
Helpers
Pure, React-free helpers exported from gantt-lib.tsx, the gesture utilities from gantt-dnd.tsx, and mergeGanttI18n from gantt-i18n.tsx. gantt-lib.tsx also exports the advanced types GanttIndex, BuildIndexOptions, ViewRangeOptions, ViewDateRanges, and WeekStartsOn used in these signatures, and gantt.tsx exports the GanttContext and GanttViewConfigContext context objects for advanced composition.
Deep-partial i18n merge; replaces individual keys, never sections.
wasRecentDrag
() => boolean
True within 250ms of a gesture end, so click handlers can ignore the click that ends a drag.
markGestureEnd
() => void
Mark a non-dnd gesture (e.g. a timeline pan) so the click it ends is ignored.
Exported constants: GANTT_SCALES (the five scales in menu order), GANTT_COLORS (ten named Tailwind palette presets for bar colors), GANTT_ACTIVATION (the default activation thresholds), MIN_PACK_SLOT (30, packing-effective minimum minutes), MAX_OCCURRENCES (1000, recurrence expansion cap per event), DEFAULT_GANTT_I18N (the default i18n config), and DEFAULT_VIEW_CONFIG (the default view configuration).
Recurrence
Exported from gantt-recurrence.tsx. The built-in expander covers the RFC 5545 subset described under GanttRecurrenceRule; unsupported parts throw GanttRecurrenceError instead of silently mis-expanding, and expansion caps at MAX_OCCURRENCES (1000) per event.
Rendered range changed (fires once on mount); fetch remote data here.
onScaleChange
(scale: GanttScale) => void
Scale changed.
onDateChange
(date: Date) => void
Anchor date changed (navigation or an infinite-scroll anchor slide).
onSelectionChange
(selection: GanttSelection) => void
Selection changed.
onInteractionsChange
(interactions: GanttInteractions) => void
Interaction switches changed.
onEventsChange
(events: GanttEvent<TData>[]) => void
Events changed (accepted updates, api mutations).
onResourceReorder
(proposal: GanttResourceReorder) => void | false
Commit gate for tree-row drag reorder; adopt proposal.resources into your resources state, return false to reject.
canReorderResource
(proposal: GanttResourceReorder) => boolean
Live validity predicate while a resource row is being dragged.
onResourceReorderReject
(proposal: GanttResourceReorder) => void
Fires when a reorder gesture is released on a rejected position; explain the rejection (a toast).
View configuration
Display props and render overrides. These live on <Gantt> as flat props (and GanttView accepts interval directly), never in the headless options; view components read them via useGanttViewConfig.
Prop
Type
Default
Description
nowIndicator
boolean
true
Red now-line on the axis.
interval
number
60
Day-scale unit interval in minutes; axis units and gridlines follow it.
scrollbars
"custom" | "native"
"custom"
Scroll implementation for the gantt body: shadcn ScrollArea or browser scrollbars.
displayScheduleHint
boolean
false
Placement hint over empty timeline track: a validated, snapped tile that opens the schedule flow.
dragCreate
boolean
false
Empty-track presses start a drag-create gesture committing through onSelectSlot; off means the panel drags-to-pan.
displayCreateTaskHint
boolean
false
"Add task" affordance at the foot of the tree; shown only when canCreateTask allows it.
zoomControl
boolean
true
Floating zoom in/out control over the track.
wheelZoom
boolean
true
Ctrl/Cmd + wheel over the timeline zooms the range, anchored on the pointer; trackpad pinch arrives as the same event. At the zoom limits the gesture returns to the browser so page zoom still works.
navButtonVariant
"ghost" | "outline" | "secondary" | "default"
"ghost"
Nav button variant; all nav buttons follow it.
navButtonSize
"sm" | "default"
"sm"
Nav button size; icon buttons use the icon twin.
offDays
boolean | GanttOffDaysConfig
true
Off-day marking on day/week/month scales; true = weekends with a muted background, an object customizes it.
columns
GanttColumn[]
-
Extra tree-panel columns after the built-in name column; the panel scrolls horizontally, the name column stays pinned.
columnsMenu
ReactNode
-
Consumer slot pinned at the end of the tree-panel header, the intended home for a columns dropdown.
treePanel
GanttTreePanelConfig
-
Tree-panel width, splitter bounds, and resizability.
timelineLines
"vertical" | "both" | "none"
"vertical"
Timeline gridlines: unit boundaries only, plus row borders, or bare.
barLabel
"inside" | "outside" | "auto"
"inside"
Bar title placement; "auto" moves it outside only when the bar is too short.
offscreenIndicators
boolean
true
Edge chips that scroll to bars outside the visible timeline.
infiniteScroll
boolean
true
Extend the timeline into the past/future while scrolling near an edge (the anchor period stays the nav title).
zoomRange
{ min?: number; max?: number; step?: number }
0.5 - 3, step 0.25
Zoom bounds and button step for the floating control.
Replaces the rollup math: return 0-100 (or null to hide). Default: duration-weighted mean progress.
GanttInteractions
Global interaction switches; per-event readOnly, draggable, and resizable override them.
Property
Type
Default
Description
drag
boolean
true
Horizontal move within the bar's own row; never across rows.
resize
boolean
true
Edge resize on bar segments.
selectSlot
boolean
true
Slot selection (drag-create and slot clicks).
GanttOffDaysConfig
Off-day (non-working day) marking. true uses the defaults: weekends with a muted background. Marked cells carry data-off for CSS-selector customization.
Property
Type
Default
Description
weekendDays
number[]
[0, 6]
Weekday numbers treated as off (0 = Sunday).
dates
Date[]
-
Additional explicit off dates (compared by day in the display zone).
isOffDay
(day: Date) => boolean
-
Full custom predicate; runs in addition to weekendDays/dates.
className
string
"bg-muted/40"
Marker classes.
GanttTreePanelConfig
Left tree-panel sizing and splitter behavior.
Property
Type
Default
Description
width
number
288
Initial panel width in px.
minWidth
number
180
Splitter lower bound in px.
maxWidth
number
640
Splitter upper bound in px.
resizable
boolean
true
Drag/keyboard splitter between the panels.
nameColumnWidth
number
208
Width of the sticky name column in px.
onWidthChange
(width: number) => void
-
Fires after any user resize (drag release, keyboard, double-click reset).
GanttColumn
One extra tree-panel column after the built-in name column.
Property
Type
Default
Description
id
string
-
Required. Stable id; doubles as the default header label.
title
ReactNode
-
Header label.
width
number
96
Fixed column width in px.
align
"start" | "center" | "end"
"start"
Cell content alignment.
render
(ctx: GanttColumnContext) => ReactNode
-
Cell content per row; omit or return null for an empty cell.
className
string
-
Extra classes on every cell of this column (header included).
GanttMetrics
Layout metrics (rem unless noted); every knob falls back to its default.
Property
Type
Default
Description
laneHeight
number
1.25
Bar lane height.
laneGap
number
0.1875
Gap between stacked lanes in one row.
rowPadding
number
0.5
Vertical padding around a row's lane block.
minRowHeight
number
2.5
Minimum row height.
ghostHeight
number
1.25
Drag-drop indicator height, centered in its lane band and published on the ghost as --gantt-ghost-height.
autoLabelMin
number
7
barLabel"auto" flips the title outside below this bar width.
unitWidths
Partial<Record<GanttScale, number>>
-
Unit width at zoom 1 per scale (day scale = width per interval unit; defaults: day 5 at a 60-minute interval, week 10, month 4, quarter 8, year 10).
minTimelineWidth
number
200
Minimum timeline pane width in px.
infiniteScrollEdge
number
160
Scroll distance (px) from an edge that grows the range.
GanttActivationConfig
Pointer-activation thresholds; unset keys keep the dnd-kit parity defaults.
Property
Type
Default
Description
moveDistancePx
number
5
Mouse travel (px) before a bar move starts.
createDistancePx
number
4
Mouse travel (px) before a drag-create starts.
touchDelayMs
number
250
Touch long-press delay in ms.
touchTolerancePx
number
5
Touch movement tolerance (px) during the long-press.
GanttClassNames
Class overrides for the composed parts.
Property
Type
Default
Description
nav
string
-
Classes for GanttNav.
toolbar
string
-
Classes for GanttToolbar.
view
string
-
Classes for the gantt body (tree + track).
event
string
-
Classes for every bar.
baseline
string
-
Classes for every planned-window (baseline) ghost.
dependencies
string
-
Classes for the dependency-arrow SVG layer (color flows from currentColor).
Internationalization
The i18n option takes a GanttI18nOverrides object: a deep-partial of GanttI18nConfig where a partial override replaces individual keys, never whole sections (merged by mergeGanttI18n, defaults in DEFAULT_GANTT_I18N).
labels keys and defaults:
Property
Type
Default
today
string
"Today"
previous
string
"Previous"
next
string
"Next"
addEvent
string
"Add event"
addTask
string
"Add task"
allDay
string
"All day"
loading
string
"Loading events"
event
string
"event"
events
(count: number) => string
"1 event" / `${count} events`
week
(weekNumber: number) => string
`W${weekNumber}`
resources
string
"Resources"
goToDate
string
"Go to date"
scheduleHint
string
"Click to add a schedule"
reorder
string
"Reorder"
selectView
string
"Select view"
zoomIn
string
"Zoom in"
zoomOut
string
"Zoom out"
resizePanel
string
"Resize panel"
jumpToBar
(title: string) => string
`Scroll to "${title}"`
progress
(percent: number) => string
`${percent}% complete`
durationDays
(days: number) => string
"1 day" / `${days} days`
continues
string
"continues"
planned
(rangeLabel: string) => string
`Planned ${rangeLabel}`
milestone
string
"milestone"
scales
Record<GanttScale, string>
Day, Week, Month, Quarter, Year
formats are date-fns format strings, applied with the gantt locale:
Property
Default
Description
monthTitle
"MMMM yyyy"
Month-scale title.
dayTitle
"EEEE, MMMM d, yyyy"
Day-scale title and Today tooltip.
timeGutter
"h a"
Day-scale axis unit labels.
eventTime
"h:mm a"
Timed event time labels.
functions are the composed formatters. The defaults are re-bound to the merged labels/formats on every merge, so overriding a format string (for example formats.eventTime) reaches the default renderers without also replacing the function:
Composes the bar screen-reader label; appends labels.continues when clipped.
Shadcn Gantt Free Components
Browse 7 production-ready Shadcn Gantt components for dashboards, forms, and product UI. These examples follow the Radix UI implementation with accessible primitives from the Radix stack and stay fully compatible with Shadcn Create so radius, color, and typography match your configured theme.
This primitive also powers ready-made ReUI Pro blocks: complete gantt sections assembled on top of the exact component documented here, with realistic data, polished layout details, and production interaction patterns you can copy straight into your app.