Gantt
freeGantt plots tasks, milestones, and dependencies across a timeline, with drag-to-reschedule, resize-to-adjust-duration, and drag-to-set-progress built in.
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
const initialTasks: Task[] = [
{
id: 'discovery',
name: 'Discovery',
type: 'task',
start: on(1),
end: on(5),
progress: 100,
displayOrder: 1,
},
{
id: 'design',
name: 'Design',
type: 'task',
start: on(5),
end: on(11),
progress: 80,
dependencies: ['discovery'],
displayOrder: 2,
},
{
id: 'build',
name: 'Build',
type: 'task',
start: on(11),
end: on(20),
progress: 45,
dependencies: ['design'],
displayOrder: 3,
},
{
id: 'qa',
name: 'QA & polish',
type: 'task',
start: on(18),
end: on(25),
progress: 15,
dependencies: ['build'],
displayOrder: 4,
},
{
id: 'launch',
name: 'Launch',
type: 'milestone',
start: on(27),
end: on(27),
progress: 0,
dependencies: ['qa'],
displayOrder: 5,
},
];
export default function UsageDemo() {
const [tasks, setTasks] = useState(initialTasks);
const update = (task: Task) =>
setTasks((prev) =>
prev.map((t) =>
t.id === task.id
? { ...t, start: task.start, end: task.end, progress: task.progress }
: t,
),
);
return (
<div className="w-full">
<Gantt
viewMode="Week"
tasks={tasks}
onDateChange={update}
onProgressChange={update}
/>
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add GanttExamples
Every example below shares the same chart interactions: hold Space and drag to pan, hover a row to highlight it in both the table and the chart, and scrollbars stay hidden until the pointer is over the chart.
View Modes
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import Segment from '@/components/ui/Segment';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
const tasks: Task[] = [
{
id: 'plan',
name: 'Planning',
type: 'task',
start: on(2),
end: on(8),
progress: 100,
displayOrder: 1,
},
{
id: 'build',
name: 'Build',
type: 'task',
start: on(8),
end: on(20),
progress: 60,
dependencies: ['plan'],
displayOrder: 2,
},
{
id: 'ship',
name: 'Ship',
type: 'milestone',
start: on(22),
end: on(22),
progress: 0,
dependencies: ['build'],
displayOrder: 3,
},
];
const views = ['Day', 'Week', 'Month', 'Year'] as const;
export default function ViewModesDemo() {
const [view, setView] = useState<(typeof views)[number]>('Week');
return (
<div className="w-full">
<div className="p-4 flex justify-end border-b">
<Segment
value={view}
onChange={(value) => value && setView(value as typeof view)}
>
{views.map((option) => (
<Segment.Item key={option} value={option}>
{option}
</Segment.Item>
))}
</Segment>
</div>
<Gantt viewMode={view} tasks={tasks} />
</div>
);
}Off Days
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
const tasks: Task[] = [
{
id: 'design',
name: 'Design',
type: 'task',
start: on(1),
end: on(8),
progress: 100,
displayOrder: 1,
},
{
id: 'build',
name: 'Build',
type: 'task',
start: on(8),
end: on(18),
progress: 55,
dependencies: ['design'],
displayOrder: 2,
},
{
id: 'launch',
name: 'Launch',
type: 'milestone',
start: on(20),
end: on(20),
progress: 0,
dependencies: ['build'],
displayOrder: 3,
},
];
export default function OffDaysDemo() {
const [items] = useState(tasks);
return (
<div className="w-full">
<Gantt viewMode="Day" tasks={items} offDays={[5, 6]} />
</div>
);
}Grouped Tasks
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
const initialTasks: Task[] = [
{
id: 'phase-1',
name: 'Phase 1 · Foundations',
type: 'project',
start: on(1),
end: on(12),
progress: 70,
hideChildren: false,
displayOrder: 1,
},
{
id: 'data-model',
name: 'Data model',
type: 'task',
project: 'phase-1',
start: on(1),
end: on(5),
progress: 100,
displayOrder: 2,
},
{
id: 'auth',
name: 'Authentication',
type: 'task',
project: 'phase-1',
start: on(5),
end: on(12),
progress: 60,
dependencies: ['data-model'],
displayOrder: 3,
},
{
id: 'phase-2',
name: 'Phase 2 · Product',
type: 'project',
start: on(12),
end: on(26),
progress: 20,
hideChildren: false,
displayOrder: 4,
},
{
id: 'dashboard',
name: 'Dashboard',
type: 'task',
project: 'phase-2',
start: on(12),
end: on(20),
progress: 30,
displayOrder: 5,
},
{
id: 'reports',
name: 'Reports',
type: 'task',
project: 'phase-2',
start: on(20),
end: on(26),
progress: 0,
dependencies: ['dashboard'],
displayOrder: 6,
},
];
export default function GroupedTasksDemo() {
const [tasks, setTasks] = useState(initialTasks);
const handleExpanderClick = (task: Task) =>
setTasks((prev) =>
prev.map((t) =>
t.id === task.id ? { ...t, hideChildren: task.hideChildren } : t,
),
);
return (
<div className="w-full">
<Gantt
viewMode="Week"
tasks={tasks}
onExpanderClick={handleExpanderClick}
/>
</div>
);
}Add & Edit Tasks
onCellClick fires with the row's task and the clicked date when an empty
timeline cell is clicked; onDoubleClick fires with the task when a bar is
double-clicked. Neither prop renders any UI on its own — wire them to open
whatever your app needs, like the dialog below.
import { useState } from 'react';
import Button from '@/components/ui/Button';
import DatePicker from '@/components/ui/DatePicker';
import Dialog from '@/components/ui/Dialog';
import Form from '@/components/ui/Form';
import Input from '@/components/ui/Input';
import Slider from '@/components/ui/Slider';
import Gantt from '@/components/composites/Gantt';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
const addDays = (date: Date, days: number) => {
const next = new Date(date);
next.setDate(next.getDate() + days);
return next;
};
const initialTasks: Task[] = [
{
id: 'kickoff',
name: 'Kickoff',
type: 'task',
start: on(1),
end: on(4),
progress: 100,
displayOrder: 1,
},
{
id: 'design',
name: 'Design',
type: 'task',
start: on(4),
end: on(11),
progress: 80,
dependencies: ['kickoff'],
displayOrder: 2,
},
{
id: 'build',
name: 'Build',
type: 'task',
start: on(11),
end: on(22),
progress: 45,
dependencies: ['design'],
displayOrder: 3,
},
{
id: 'qa',
name: 'QA & polish',
type: 'task',
start: on(20),
end: on(27),
progress: 15,
dependencies: ['build'],
displayOrder: 4,
},
{
id: 'beta',
name: 'Beta rollout',
type: 'task',
start: on(27),
end: on(33),
progress: 0,
dependencies: ['qa'],
displayOrder: 5,
},
{
id: 'launch',
name: 'Launch',
type: 'milestone',
start: on(35),
end: on(35),
progress: 0,
dependencies: ['beta'],
displayOrder: 6,
},
];
type DialogState =
| { mode: 'create'; date: Date }
| { mode: 'edit'; task: Task };
type TaskFormValues = { name: string; start: Date; end: Date; progress: number };
function TaskDialogForm({
state,
onCancel,
onSubmit,
onDelete,
}: {
state: DialogState;
onCancel: () => void;
onSubmit: (values: TaskFormValues) => void;
onDelete: () => void;
}) {
const initial: TaskFormValues =
state.mode === 'edit'
? {
name: state.task.name,
start: state.task.start,
end: state.task.end,
progress: state.task.progress,
}
: { name: '', start: state.date, end: addDays(state.date, 3), progress: 0 };
const [name, setName] = useState(initial.name);
const [start, setStart] = useState<Date | null>(initial.start);
const [end, setEnd] = useState<Date | null>(initial.end);
const [progress, setProgress] = useState(initial.progress);
return (
<Form
onSubmit={(event) => {
event.preventDefault();
if (!start || !end) return;
onSubmit({ name, start, end, progress });
}}
>
<div className="border-b px-4 py-4">
<h5 className="text-lg font-semibold text-foreground">
{state.mode === 'create' ? 'Add task' : 'Edit task'}
</h5>
</div>
<div className="space-y-4 p-4">
<Form.Field label="Name" htmlFor="gantt-dialog-name" asterisk>
<Input
id="gantt-dialog-name"
required
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="Task name"
/>
</Form.Field>
<div className="grid grid-cols-2 gap-4">
<Form.Field label="Start" htmlFor="gantt-dialog-start">
<DatePicker value={start} onChange={setStart} />
</Form.Field>
<Form.Field label="End" htmlFor="gantt-dialog-end">
<DatePicker value={end} onChange={setEnd} />
</Form.Field>
</div>
<Form.Field label={`Progress — ${progress}%`} htmlFor="gantt-dialog-progress">
<Slider value={progress} onChange={setProgress} />
</Form.Field>
</div>
<div className="flex items-center justify-end gap-2 px-4 py-3">
{state.mode === 'edit' && (
<Button type="button" destructive onClick={onDelete} className="mr-auto">
Delete
</Button>
)}
<Button type="button" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" variant="solid">
{state.mode === 'create' ? 'Add' : 'Save'}
</Button>
</div>
</Form>
);
}
export default function AddEditTasksDemo() {
const [tasks, setTasks] = useState(initialTasks);
const [dialogState, setDialogState] = useState<DialogState | null>(null);
const closeDialog = () => setDialogState(null);
return (
<div className="w-full">
<Gantt
viewMode="Week"
tasks={tasks}
onCellClick={(_task, date) => setDialogState({ mode: 'create', date })}
onDoubleClick={(task) => setDialogState({ mode: 'edit', task })}
/>
<Dialog
isOpen={dialogState !== null}
onClose={closeDialog}
className="p-0"
>
{dialogState && (
<TaskDialogForm
key={dialogState.mode === 'edit' ? dialogState.task.id : 'create'}
state={dialogState}
onCancel={closeDialog}
onSubmit={(values) => {
if (dialogState.mode === 'create') {
setTasks((prev) => [
...prev,
{
id: `task-${Date.now()}`,
type: 'task',
displayOrder: prev.length + 1,
...values,
},
]);
} else {
const editing = dialogState.task;
setTasks((prev) =>
prev.map((t) => (t.id === editing.id ? { ...t, ...values } : t)),
);
}
closeDialog();
}}
onDelete={() => {
if (dialogState.mode !== 'edit') return;
const editing = dialogState.task;
setTasks((prev) => prev.filter((t) => t.id !== editing.id));
closeDialog();
}}
/>
)}
</Dialog>
</div>
);
}Resizable Bars
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
const formatDate = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' });
const initialTasks: Task[] = [
{
id: 'draft',
name: 'Draft',
type: 'task',
start: on(1),
end: on(7),
progress: 100,
displayOrder: 1,
},
{
id: 'review',
name: 'Review',
type: 'task',
start: on(7),
end: on(13),
progress: 55,
dependencies: ['draft'],
displayOrder: 2,
},
{
id: 'approve',
name: 'Approve',
type: 'task',
start: on(13),
end: on(17),
progress: 20,
dependencies: ['review'],
displayOrder: 3,
},
{
id: 'publish',
name: 'Publish',
type: 'task',
start: on(17),
end: on(21),
progress: 0,
dependencies: ['approve'],
displayOrder: 4,
},
];
export default function ResizableBarsDemo() {
const [tasks, setTasks] = useState(initialTasks);
const update = (task: Task) =>
setTasks((prev) =>
prev.map((t) =>
t.id === task.id
? { ...t, start: task.start, end: task.end, progress: task.progress }
: t,
),
);
return (
<Gantt
viewMode="Week"
tasks={tasks}
onDateChange={update}
onProgressChange={update}
/>
);
}Resizable Task List
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import Avatar from '@/components/ui/Avatar';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
type TaskMeta = {
owner: string;
ownerImage: string;
};
const initialTasks: Task<TaskMeta>[] = [
{
id: 'research',
name: 'User research',
type: 'task',
start: on(1),
end: on(6),
progress: 100,
owner: 'Mira Okafor',
ownerImage: '/img/avatars/thumb-11.jpg',
displayOrder: 1,
},
{
id: 'flows',
name: 'Flow design',
type: 'task',
start: on(6),
end: on(13),
progress: 70,
owner: 'Dan Levy',
ownerImage: '/img/avatars/thumb-12.jpg',
dependencies: ['research'],
displayOrder: 2,
},
{
id: 'build',
name: 'Implementation',
type: 'task',
start: on(13),
end: on(22),
progress: 30,
owner: 'Priya Shah',
ownerImage: '/img/avatars/thumb-13.jpg',
dependencies: ['flows'],
displayOrder: 3,
},
{
id: 'ship',
name: 'Ship v2',
type: 'milestone',
start: on(24),
end: on(24),
progress: 0,
owner: 'Team',
ownerImage: '/img/avatars/thumb-14.jpg',
dependencies: ['build'],
displayOrder: 4,
},
];
export default function ResizableTaskListDemo() {
const [tasks] = useState(initialTasks);
return (
<div className="w-full">
<Gantt<TaskMeta>
viewMode="Week"
tasks={tasks}
defaultTaskListWidth={280}
taskListMinWidth={160}
taskListMaxWidth={480}
columns={[
{
header: 'Task',
width: 180,
cell: (task) => (
<div className="flex h-full items-center px-4">
<span className="truncate font-medium">{task.name}</span>
</div>
),
},
{
header: 'Owner',
width: 150,
cell: (task) => (
<div className="flex h-full items-center gap-2 px-4">
<Avatar
size={22}
src={task.ownerImage}
alt={task.owner}
className="text-xs"
>
{task.owner.charAt(0)}
</Avatar>
<span className="truncate">
{task.owner}
</span>
</div>
),
},
{
header: 'Duration',
width: 100,
cell: (task) => {
const days = Math.round(
(Number(task.end) - Number(task.start)) / 86_400_000,
);
return (
<div className="flex h-full items-center px-4 text-muted-foreground tabular-nums">
{days > 0 ? `${days}d` : '—'}
</div>
);
},
},
]}
/>
</div>
);
}Custom Columns
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import Avatar from '@/components/ui/Avatar';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
type TaskMeta = {
owner: string;
ownerImage: string;
};
const initialTasks: Task<TaskMeta>[] = [
{
id: 'research',
name: 'User research',
type: 'task',
start: on(1),
end: on(6),
progress: 100,
owner: 'Mira Okafor',
ownerImage: '/img/avatars/thumb-11.jpg',
displayOrder: 1,
},
{
id: 'flows',
name: 'Flow design',
type: 'task',
start: on(6),
end: on(13),
progress: 70,
owner: 'Dan Levy',
ownerImage: '/img/avatars/thumb-12.jpg',
dependencies: ['research'],
displayOrder: 2,
},
{
id: 'build',
name: 'Implementation',
type: 'task',
start: on(13),
end: on(22),
progress: 30,
owner: 'Priya Shah',
ownerImage: '/img/avatars/thumb-13.jpg',
dependencies: ['flows'],
displayOrder: 3,
},
{
id: 'ship',
name: 'Ship v2',
type: 'milestone',
start: on(24),
end: on(24),
progress: 0,
owner: 'Team',
ownerImage: '/img/avatars/thumb-14.jpg',
dependencies: ['build'],
displayOrder: 4,
},
];
export default function CustomColumnsDemo() {
const [tasks] = useState(initialTasks);
return (
<div className="w-full">
<Gantt<TaskMeta>
viewMode="Week"
tasks={tasks}
columns={[
{
header: 'Task',
width: 180,
cell: (task) => (
<div className="flex h-full items-center px-4">
<span className="truncate font-medium">{task.name}</span>
</div>
),
},
{
header: 'Owner',
width: 150,
cell: (task) => (
<div className="flex h-full items-center gap-2 px-4">
<Avatar
size={22}
src={task.ownerImage}
alt={task.owner}
className="text-xs"
>
{task.owner.charAt(0)}
</Avatar>
<span className="truncate">
{task.owner}
</span>
</div>
),
},
{
header: 'Duration',
width: 100,
cell: (task) => {
const days = Math.round(
(Number(task.end) - Number(task.start)) / 86_400_000,
);
return (
<div className="flex h-full items-center px-4 text-muted-foreground tabular-nums">
{days > 0 ? `${days}d` : '—'}
</div>
);
},
},
]}
/>
</div>
);
}Custom Bar Content
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
type TaskMeta = {
styles: {
indicatorColor?: string;
progressClass?: string;
wrapperClass?: string;
};
};
const tasks: Task<TaskMeta>[] = [
{
id: 'marketing',
name: 'Marketing Revamp',
type: 'project',
start: on(1),
end: on(7),
progress: 78,
displayOrder: 1,
hideChildren: false,
styles: {
progressClass: 'fill-palette-cyan',
wrapperClass: 'fill-muted',
},
},
{
id: 'meeting-brief',
name: 'Project Brief',
type: 'task',
start: on(1),
end: on(3),
progress: 100,
project: 'marketing',
displayOrder: 2,
styles: { indicatorColor: 'var(--color-palette-cyan)' },
},
{
id: 'research',
name: 'Content Research',
type: 'task',
start: on(2),
end: on(6),
progress: 55,
project: 'marketing',
displayOrder: 3,
styles: { indicatorColor: 'var(--color-palette-cyan)' },
},
{
id: 'commerce',
name: 'Commerce Revamp',
type: 'project',
start: on(3),
end: on(9),
progress: 66,
displayOrder: 4,
hideChildren: false,
styles: {
progressClass: 'fill-palette-purple',
wrapperClass: 'fill-muted',
},
},
{
id: 'internal-meeting',
name: 'Team Sync',
type: 'task',
start: on(3),
end: on(5),
progress: 100,
project: 'commerce',
displayOrder: 5,
styles: { indicatorColor: 'var(--color-palette-purple)' },
},
{
id: 'review',
name: 'Review',
type: 'task',
start: on(5),
end: on(8),
progress: 78,
project: 'commerce',
displayOrder: 6,
styles: { indicatorColor: 'var(--color-palette-purple)' },
},
];
export default function CustomBarContentDemo() {
const [items, setItems] = useState(tasks);
const handleExpanderClick = (task: Task<TaskMeta>) => {
setItems((current) =>
current.map((item) =>
item.id === task.id
? { ...item, hideChildren: !item.hideChildren }
: item,
),
);
};
return (
<div className="w-full">
<Gantt<TaskMeta>
tasks={items}
viewMode="Day"
headerHeight={70}
rowHeight={50}
gridColumnsWidth={100}
handleWidth={8}
onExpanderClick={handleExpanderClick}
todayColor="var(--nui-primary-soft)"
barWrapperClass="stroke-border fill-accent shadow"
barProgressClass="fill-card stroke-border"
customBarContent={(task) => (
<div className="flex w-full items-center gap-1">
<span
className="h-2.5 w-2.5 shrink-0 rounded-full"
style={{ backgroundColor: task.styles.indicatorColor }}
/>
<div className="min-w-0 truncate select-none text-xs font-medium">
{task.name}
</div>
</div>
)}
/>
</div>
);
}Bar Context Menu
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import Dropdown from '@/components/ui/Dropdown';
import type { Task } from '@/components/composites/Gantt';
import {
PiPencilSimple,
PiCalendarBlank,
PiCopy,
PiTrash,
} from 'react-icons/pi';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
const initialTasks: Task[] = [
{
id: 'discovery',
name: 'Discovery',
type: 'task',
start: on(1),
end: on(6),
progress: 100,
displayOrder: 1,
},
{
id: 'design',
name: 'Design',
type: 'task',
start: on(6),
end: on(13),
progress: 65,
dependencies: ['discovery'],
displayOrder: 2,
},
{
id: 'checkpoint',
name: 'Design checkpoint',
type: 'milestone',
start: on(13),
end: on(13),
progress: 0,
dependencies: ['design'],
displayOrder: 3,
},
{
id: 'build',
name: 'Build',
type: 'project',
start: on(13),
end: on(24),
progress: 20,
displayOrder: 4,
},
{
id: 'frontend',
name: 'Frontend',
type: 'task',
project: 'build',
start: on(13),
end: on(20),
progress: 30,
displayOrder: 5,
},
{
id: 'backend',
name: 'Backend',
type: 'task',
project: 'build',
start: on(17),
end: on(24),
progress: 10,
dependencies: ['frontend'],
displayOrder: 6,
},
];
export default function BarContextMenuDemo() {
const [tasks] = useState(initialTasks);
return (
<Gantt
viewMode="Week"
tasks={tasks}
renderBarMenu={(task) => (
<>
<Dropdown.Item className="flex items-center gap-2">
<PiPencilSimple aria-hidden="true" className="text-base" />
<span>Edit {task.name}</span>
</Dropdown.Item>
<Dropdown.Menu
placement="right-start"
renderTitle={
<span className="flex items-center gap-2">
<PiCalendarBlank aria-hidden="true" className="text-base" />
<span>Reschedule</span>
</span>
}
>
<Dropdown.Item>Next working day</Dropdown.Item>
<Dropdown.Item>Next week</Dropdown.Item>
</Dropdown.Menu>
<Dropdown.Item className="flex items-center gap-2">
<PiCopy aria-hidden="true" className="text-base" />
<span>Duplicate</span>
</Dropdown.Item>
<Dropdown.Item variant="divider" />
<Dropdown.Item className="flex items-center gap-2 text-destructive hover:bg-destructive-soft hover:text-destructive">
<PiTrash aria-hidden="true" className="text-base" />
<span>Delete</span>
</Dropdown.Item>
</>
)}
/>
);
}Fixed Height
import { useState } from 'react';
import Gantt from '@/components/composites/Gantt';
import type { Task } from '@/components/composites/Gantt';
const now = new Date();
const on = (day: number) => new Date(now.getFullYear(), now.getMonth(), day);
const names = [
'Kickoff',
'Requirements',
'Wireframes',
'Visual design',
'Frontend',
'Backend',
'Integration',
'QA',
'Beta',
'Launch',
];
const tasks: Task[] = names.map((name, index) => ({
id: `task-${index}`,
name,
type: 'task',
start: on(1 + index * 2),
end: on(4 + index * 2),
progress: Math.max(0, 100 - index * 12),
displayOrder: index + 1,
...(index > 0 ? { dependencies: [`task-${index - 1}`] } : {}),
}));
export default function FixedHeightDemo() {
const [items] = useState(tasks);
return (
<div className="w-full">
<Gantt viewMode="Week" tasks={items} ganttHeight={280} />
</div>
);
}API
Gantt
| Prop | Description | Type | Default |
|---|---|---|---|
tasks | Rows to render: tasks, milestones, and project groups. Required. | Task<T>[] | — |
columns | Task-list columns shown on the left. An empty array renders the default Name column; null hides the task list and shows the timeline only. | Columns<T>[] | null | [] |
customBarContent | Renders custom content inside each task bar's inset label area. Called for task-type rows only. Not the route for a menu — use renderBarMenu. | CustomBarContent<T> | — |
renderBarMenu | Menu content for a bar's right-click menu. Return Dropdown.Item children. Applies to tasks, milestones, and project rows. | (task: Task<T>) => ReactNode | — |
viewMode | Time scale of the timeline. | ViewMode | 'Day' |
viewDate | Scrolls the timeline so this date is in view. | Date | — |
scrollToTodayOnLoad | Scrolls to today on first render when today falls inside the data range. Ignored when viewDate is set. | boolean | true |
ref | Imperative handle for scrolling the timeline. See GanttRef. | Ref<GanttRef> | — |
preStepsCount | Empty time columns kept before the first task. | number | 1 |
locale | Locale used for the header's day and month labels. | string | 'en-GB' |
rtl | Lays the chart out right-to-left. | boolean | false |
className | Extra classes on the Gantt wrapper element. | string | — |
rowHeight | Height of each row, in px. | number | 50 |
gridColumnsWidth | Width of each time column, in px. | number | 100 |
headerHeight | Height of the date header, in px. | number | 70 |
ganttHeight | Fixed chart height in px; rows scroll vertically inside it. 0 grows to fit every row. | number | 0 |
resizableTaskList | Lets the user drag the divider between the task list and the timeline. | boolean | true |
defaultTaskListWidth | Initial task-list width in px. Omit to size to the columns' content. | number | — |
taskListMinWidth | Lower clamp when dragging the task list, in px. | number | 160 |
taskListMaxWidth | Upper clamp when dragging the task list, in px. | number | 720 |
resizableColumns | Lets the user drag the boundary between task-list columns. | boolean | true |
barFill | Percentage of the row height a bar occupies. | number | 60 |
barCornerRadius | Corner radius of bars, in px. | number | 4 |
handleWidth | Width of the move/resize handles, in px. | number | 8 |
barProgressClass | SVG fill-* class for a task bar's progress fill. | string | 'fill-primary' |
barWrapperClass | SVG fill-* class for a task bar's track. | string | 'fill-[#b8c2cc]' |
projectProgressClass | Progress fill for project rows. | string | 'fill-primary' |
projectWrapperClass | Track fill for project rows. | string | 'fill-[#b8c2cc]' |
milestoneClass | Fill for milestone diamonds. | string | 'fill-primary' |
todayColor | Fill color of the current-day column. | string | 'var(--nui-muted)' |
offDays | Weekday indices shaded as non-working, 0 = Sunday. Only applies to Day and finer view modes. [] disables. | number[] | [0, 6] |
offDayClass | SVG fill-* class for non-working columns. Non-working columns show a diagonal hatch by default; passing this replaces the hatch with a flat wash. | string | '' |
rollUpParents | Draws project rows spanning their children instead of their own dates. Dates only — a project's own progress stays as authored. Computed for display; never written to your data. | boolean | false |
arrowClass | Extra class on dependency arrows, which otherwise inherit border. | string | '' |
arrowIndent | Horizontal indent of dependency arrows, in px. | number | 20 |
timeStep | Snap interval for drag and resize, in ms. | number | 300000 |
TooltipContent | Component rendered in the hover/drag tooltip. | FC<{ task: Task<T> }> | StandardTooltipContent |
pushDependents | Moves a dragged task's dependents with it — live during the drag, committed on drop. Only pushes later, never pulls earlier. | boolean | false |
onDateChange | Fires after a bar is moved or resized. Return false to reject the change — with pushDependents on, this reverts every task that moved, not just the dragged one. Providing this also enables the move and resize handles. The second argument is the task's direct successors when pushDependents is off (informational only); every task that actually moved when it's on. | (task: Task<T>, children: Task<T>[]) => void | boolean | Promise<void | boolean> | — |
onProgressChange | Fires after the progress handle is dragged. Return false to reject. Providing this also enables the progress handle. | (task: Task<T>, children: Task<T>[]) => void | boolean | Promise<void | boolean> | — |
onExpanderClick | Fires when a project's expander is toggled. Providing this also hides the children of collapsed projects. | (task: Task<T>) => void | — |
onClick | Fires when a bar is clicked. | (task: Task<T>) => void | — |
onDoubleClick | Fires when a bar is double-clicked. | (task: Task<T>) => void | — |
onSelect | Fires when a bar gains or loses selection (focus). | (task: Task<T>, isSelected: boolean) => void | — |
onDelete | Fires when Delete is pressed on a focused bar. Return truthy to remove it. | (task: Task<T>) => void | boolean | Promise<void | boolean> | — |
onCellClick | Fires when an empty timeline cell is clicked. Receives the row's task and the clicked column's start date. Providing this also enables the cell hover highlight and pointer cursor. | (task: Task<T>, date: Date, event: MouseEvent) => void | — |
onBarContextMenu | Fires on right-click of a bar, before the menu opens. | (task: Task<T>, event: MouseEvent) => void | — |
The move and resize handles only appear when onDateChange is set, and the progress handle only appears when onProgressChange is set. The chart keeps its own internal copy of the rows, so a bar stays where it is dragged even before the callback updates your data; return false from the callback to snap it back. With custom columns, the built-in expander button is not rendered — wire the cell's expander node to your own handler when you need clickable groups.
Types
type ViewMode =
| 'Hour'
| 'QuarterDay'
| 'Half Day'
| 'Day'
| 'Week'
| 'Month'
| 'QuarterYear'
| 'Year';
type TaskType = 'task' | 'milestone' | 'project';
type Task<T = object> = {
id: string;
type: TaskType;
name: string;
start: Date;
end: Date;
progress: number; // 0-100
dependencies?: string[]; // ids of tasks this row follows
project?: string; // parent project id
hideChildren?: boolean; // collapse a project's children
displayOrder?: number; // row sort order
isDisabled?: boolean; // lock a row from editing
styles?: { progressClass?: string; wrapperClass?: string };
} & T;
type Columns<T = object> = {
header: string | ReactNode;
cell: (task: Task<T> & { expander: ReactNode }) => string | ReactNode;
width?: string | number;
};
type CustomBarContent<T = object> = (task: BarTask<T>) => ReactNode;
// BarTask is a Task extended with its computed bar geometry.
type BarTask<T = object> = Task<T> & {
index: number;
x1: number;
x2: number;
y: number;
height: number;
progressX: number;
progressWidth: number;
barChildren: BarTask<T>[];
};
type GanttRef = {
today: () => void; // scroll today's column to the left edge (no-op if out of range)
goTo: (date: Date) => void; // scroll the given date's column to the left edge (no-op if out of range)
next: () => void; // scroll forward one viewport width
prev: () => void; // scroll back one viewport width
scrollToTask: (taskId: string) => void; // scroll the given task's start into view (no-op if unknown)
};