Task data
The `Task` shape, date handling, and how the `tasks` prop is compared
Your rows come out of a database, and every field name is different from the one the chart wants. Before you map anything you need to know which fields are mandatory, which are inert until you turn a prop on, and what happens when a value arrives malformed. The chart runs no validation at all, so a wrong field does not raise an error — it draws something wrong instead. This page is the contract for the array you hand to tasks.
The minimum that renders
Six fields are required on every task. Everything else is optional.
// src/TaskDataExample.tsx
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
import '@jaeungkim/gantt-chart/style.css';
const tasks: Task[] = [
{
id: 'design',
name: 'Design',
startDate: '2025-06-01',
endDate: '2025-06-10',
parentId: null,
sequence: '1',
},
{
id: 'build',
name: 'Build',
startDate: '2025-06-10',
endDate: '2025-06-24',
parentId: null,
sequence: '2',
progress: 40,
dependencies: [{ targetId: 'design', type: 'FS' }],
},
];
export function TaskDataExample() {
return <ReactGanttChart tasks={tasks} height={400} />;
}tasks itself is optional on the component. Omitting it, or passing [], renders an empty chart.
Task fields
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
id | string | yes | — | Identity. Dependencies, roll-up and undo patches resolve through it. Uniqueness is never checked. |
name | string | yes | — | Label on the bar, in the task list cell, and in the ARIA label. Rendered as text. |
startDate | string | yes | — | Left edge of the bar. Any string dayjs can parse; see Dates and time zones. |
endDate | string | yes | — | Right edge of the bar. Ignored for a milestone, with one exception below. |
parentId | string | null | yes | — | Id of the parent task, or null for a root. Only read when hierarchy is on. |
sequence | string | yes | — | Dot-separated numbers, '1', '2.1', '2.10'. Decides row order. |
type | 'task' | 'milestone' | no | 'task' | 'milestone' draws a diamond at startDate. |
progress | number | no | none | Percentage 0–100. Omitted means no progress fill. |
color | string | no | theme tokens | Any CSS color. The progress fill and the hover shade are derived from it. |
className | string | no | none | Extra class on the bar element and on the task list row. |
lane | string | no | own row | Tasks sharing a lane are drawn side by side on one row. An empty string counts as absent. |
dependencies | TaskDependency[] | no | [] | The predecessors this task waits on. |
readOnly | boolean | no | none | Blocks every gesture on this task. |
allowMove | boolean | no | none | Allows or blocks dragging the bar sideways. |
allowResize | boolean | no | none | Allows or blocks dragging either edge. |
allowProgressChange | boolean | no | none | Allows or blocks the progress handle. |
allowLinkCreate | boolean | no | none | Allows or blocks starting a dependency drag from this task. |
allowLinkDelete | boolean | no | none | Allows or blocks deleting a dependency this task owns. |
minDate | string | no | the chart's minDate | Earliest date a drag may land on. |
maxDate | string | no | the chart's maxDate | Latest date a drag may land on. |
manuallyScheduled | boolean | no | false | The scheduling engine never moves this task. It still constrains its successors. |
baselineStart | string | no | none | Planned start snapshot, drawn as a thin bar under the live one. Alone, it draws a single point. |
baselineEnd | string | no | none | Planned end snapshot. Without baselineStart it is ignored entirely. |
The five allowX flags and readOnly resolve most-specific-first, task.allowX before task.readOnly before the chart's config; see Editing tasks. minDate and maxDate do not use that chain — the task's value replaces the chart's when present. color and className and the CSS variables a colored bar sets are covered in Custom rendering. lane is covered in Grouping and swimlanes.
Fields that do nothing on their own
Three fields are read only under a prop. lane and the two baseline fields are not.
| Field | What it needs |
|---|---|
parentId | hierarchy — without it no tree is built, depth comes from sequence, and nothing is a summary row. See Task list and hierarchy. |
manuallyScheduled | schedulingPolicy set to something other than 'off'. See Scheduling. |
TaskDependency.lag | schedulingPolicy, or criticalPath. A lag: 5 link draws exactly like lag: 0 on a chart with neither. |
lane | nothing. Packing runs with or without groupBy; with groupBy on, a lane packs inside its own group only. See Grouping and swimlanes. |
baselineStart / baselineEnd | nothing. A baseline renders as soon as baselineStart is present, and it widens the timeline range. There is no prop gate and no way to switch it off other than removing the field. See Scheduling. |
manuallyScheduled does not stop the user dragging the bar. It only stops the scheduling engine moving it.
TaskDependency fields
A dependency lives on the successor and names its predecessor.
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
targetId | string | yes | — | The predecessor's id. |
type | 'FS' | 'SS' | 'FF' | 'SF' | yes | — | First letter is the predecessor's end, second is the successor's end. |
lag | number | no | 0 | Signed days. Positive is lag, negative is a lead. Working days when workingCalendar is on. |
What the four types mean for scheduling, and how arrows are drawn and deleted, is in Dependencies.
Two entries are dropped without a warning: one whose targetId is not in tasks, and one whose targetId is the task's own id. An unrecognized type skips that arrow, and logs a console.warn once per distinct value in a development build only. In a production bundle the arrow is missing with no signal at all.
Dates and time zones
The chart parses every date through a dayjs instance locked to UTC mode. The timezone plugin is not loaded, so there is no per-viewer local mode to opt into.
| Input | What is displayed |
|---|---|
'2025-06-01T09:00:00Z' | that instant at UTC clock time, 09:00 |
'2025-06-01T18:00:00+09:00' | the same instant, also 09:00 |
'2025-06-01T09:00' | read as a UTC wall clock, so 09:00 in every viewer's time zone |
'2025-06-01' | 2025-06-01T00:00:00.000Z, UTC midnight |
A bare YYYY-MM-DD is therefore safe. It lands on the day it names for every viewer, in Seoul and in Los Angeles alike.
A UTC ISO string is the shape the chart emits, but it is not a requirement on input. Any string dayjs can parse works, and a string without a zone is read as a UTC wall clock rather than rejected or shifted. Because the grid is UTC, every hour cell is 60 minutes wide and every day cell is the same width, even across a local 23- or 25-hour daylight-saving day.
There is no date validation on tasks. An unparseable startDate does not hide the row and does not throw. Its bar starts at the left edge of the timeline and spans the whole width. Markers and range bands are validated, so the same bad string is silently dropped as a marker and drawn full-width as a task.
Every value the chart writes back is serialized with toISOString(). A task you passed as '2025-06-01' comes back as '2025-06-01T00:00:00.000Z' after the first drag. That matters for the diff rule below.
sequence and parentId
sequence decides row order, always. parentId decides nesting, and only when hierarchy is on. Nothing reconciles the two.
Segments are compared numerically, left to right. '1.10' sorts after '1.2', not before it. A missing segment counts as 0, so '1' and '1.0' compare equal, and ties keep the order the array was in. The input array is copied before sorting, never mutated.
A segment that is not a number becomes 0. 'abc', '1a' and '' all sort as if they were '0', ahead of '1', with no warning. A duplicated sequence is not an error either — the two rows tie, and the array order breaks the tie.
[!WARNING] A missing
sequencethrows.sequence.split('.')runs onundefinedinside the layout effect that builds the timeline, and the whole chart fails to render. TypeScript marks the field required, so this only bites hand-built JSON and data mapped without types.
With hierarchy off, indentation depth is the number of dots in sequence, so '2.1.1' renders two levels in. With hierarchy on, depth comes from the parentId chain and sequence only orders. Incoherent input renders incoherently: a child whose sequence sorts above its parent is drawn above its parent.
The library rewrites sequence in exactly one situation. A row reorder renumbers every task depth-first from the resulting tree, and writes parentId on the moved task only. Persist the whole array after a reorder, not only the row that moved. See Reordering rows.
Milestones
A milestone is a task whose type is the exact string 'milestone'. The comparison is strict, so 'Milestone' and 'MILESTONE' are ordinary tasks.
A milestone occupies a single point at startDate — its computed bar width is 1px — and draws a 16px diamond centered there. It reports zero duration to scheduling and to the critical path. It is never resizable, and no capability flag can turn that back on. It renders no progress fill. Roll-up counts it at startDate, and lane packing treats startDate as its end.
The one gap in that list is the keyboard. A milestone that already carries a numeric progress can still have it stepped from the keyboard: the value changes, onTasksChange fires, and nothing on screen moves. Only summary rows are blocked from progress editing structurally.
The sharp edge is endDate. Rendering ignores it, roll-up ignores it, and scheduling ignores it — but the function that fits the timeline to the data reads endDate unconditionally, for every task.
import type { Task } from '@jaeungkim/gantt-chart';
// This milestone renders as a diamond on 2025-02-01,
// and stretches the timeline out to 2030 with nothing drawn there.
const milestone: Task = {
id: 'launch',
name: 'Launch',
type: 'milestone',
startDate: '2025-02-01',
endDate: '2030-01-01',
parentId: null,
sequence: '3',
};The same unconditional read powers a marker's warnOnOverrun check, so a milestone can trip an overrun warning on a date it never renders at. Set endDate equal to startDate on every milestone and neither problem exists. Roll-up progress is the one other place a milestone's endDate counts, as the duration weight for a child.
progress
progress is a number from 0 to 100. Out-of-range values are clamped rather than rejected.
| Value passed | Value used |
|---|---|
42 | 42 |
33.7 | 33.7 — no rounding |
-10 | 0 |
150 | 100 |
Infinity | 100 |
-Infinity | 0 |
undefined | none — no fill drawn |
NaN | none |
'50' | none — a string is dropped, not coerced |
The last row is the one that catches people. A percentage that arrived from an API as a string is not parsed. The bar renders with no fill and nothing reports a problem.
Rounding happens only where the chart produces a value. Dragging the progress handle emits a whole number, and a summary row's rolled-up percentage is rounded. A fractional value you pass in stays fractional.
A parent's own progress always wins over the value rolled up from its children; the roll-up formula is in Task list and hierarchy.
Updating the tasks prop
You drag a bar, your onTasksChange handler writes to state, state flows back down into tasks, and the bar snaps back to where it was. Or worse: it stays put, but your undo stack is empty. Both come from the same mechanism.
The chart compares the incoming tasks array against the last one it accepted using JSON.stringify on both sides. There is no structural diff and no field-wise comparison. If the two strings match, the update is discarded and the chart keeps its own state, which is what stops your echo from reverting the drag you just made. If they differ, the chart replaces its data and wipes both undo stacks, because a different string means the host replaced the data.
String equality has two consequences that a structural diff would not have.
Key order matters. These two objects describe the same task and produce different strings.
JSON.stringify({ id: 'a', name: 'Design' }); // '{"id":"a","name":"Design"}'
JSON.stringify({ name: 'Design', id: 'a' }); // '{"name":"Design","id":"a"}'undefined-valued keys are invisible. { id: 'a', progress: undefined } and { id: 'a' } serialize identically, so switching between them is not a change at all.
Two more follow from the same code. Extra properties of your own count toward the comparison, and mutating a task in place while keeping the same array identity is invisible — the comparison never runs.
A wrong update and a right one
This handler rebuilds every task with a different key order and re-formats the dates. Every string differs from what the chart emitted, so every gesture clears the undo history it just recorded.
// wrong
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
function Wrong({ tasks, setTasks }: {
tasks: Task[];
setTasks: (next: Task[]) => void;
}) {
return (
<ReactGanttChart
tasks={tasks}
onTasksChange={(updated) => {
setTasks(
updated.map((task) => ({
name: task.name,
id: task.id,
startDate: task.startDate.slice(0, 10), // '2025-06-01T00:00:00.000Z' -> '2025-06-01'
endDate: task.endDate.slice(0, 10),
parentId: task.parentId,
sequence: task.sequence,
}))
);
}}
/>
);
}Hand back exactly what you were given. The array is already a new array, and every object in it is already in the shape the chart emitted.
// right
import { ReactGanttChart, type Task } from '@jaeungkim/gantt-chart';
function Right({ tasks, setTasks }: {
tasks: Task[];
setTasks: (next: Task[]) => void;
}) {
return (
<ReactGanttChart
tasks={tasks}
onTasksChange={(updated) => setTasks(updated)}
/>
);
}Reshape for your API inside the save call, not on the way back into the prop. When you do need to change the data yourself — inserting a task, deleting one — build the new array from the one the chart last emitted, and accept that the undo history resets. What one undo step covers, and how onBeforeTaskChange fits in, is in Events and cancellable changes.
Limits
The chart reads your array. It does not police it.
- No validation of any kind. No required-field check, no date parsing check, no id-uniqueness check, no range check on
progress, no membership check onDependencyType. The only runtime diagnostic in the whole model is one development-onlyconsole.warnfor an unknown dependency type. - Duplicate ids are never detected. Every array entry still gets a row, but every lookup keyed by id keeps the last occurrence. Dependencies, roll-up and undo patches resolve to the wrong task. Deduplicate before you pass the array in.
- No normalization on input. Dates are not re-serialized,
sequenceis not renumbered, andparentIdis not rewritten. An orphan or a cycle is treated as a root when the tree is built, but the field itself is left exactly as you wrote it. - No local time zone mode. UTC is hardcoded. Convert to and from local wall-clock time in the host app if your users need it.
- The chart never adds or removes tasks. Drawing a new task proposes a draft through
onTaskCreate, and the host suppliesid,parentIdandsequence. A change in row count is not invertible by a field patch, so it clears the undo history instead of adding a step. - There is no metadata field. Extra properties survive because the type is structural, but they are untyped and they count toward the
tasksdiff. TaskTransformedis output only. The chart hands it to your renderers and column functions;tasksonly ever acceptsTask. Its fields are listed in Task.- Nothing reconciles
sequencewithparentId. A child whosesequencesorts above its parent stays drawn above it, until the first row reorder renumbers the whole array.
Nothing is written back into the objects you passed. Every change leaves through onTasksChange as a new array, and your own copy is untouched until you replace it.
Next: Task list and hierarchy — turning parentId into summary rows, and choosing the columns beside them.