GanttColumn
`GanttColumn`
GanttColumn describes one column of the task list — its header, its width, and what each cell
draws. The columns prop takes an array of them. Import the type from the package root; see
Task list and hierarchy for when the pane appears and how the splitter behaves.
import type { GanttColumn } from '@jaeungkim/gantt-chart';GanttColumn
/**
* A column of the task grid on the left
*
* Every header label and cell body comes from here - the library hardcodes no strings.
* The first column is the tree column, so indentation and the expander attach to it.
*/
export interface GanttColumn {
/** React key, and the task field read when there is no render */
key: string;
/** What to draw in the header (a string or an element) */
header: ReactNode;
/** Column width in px (default 120) */
width?: number;
/** Cell renderer - without it, task[key] is shown as a string */
render?: (task: TaskTransformed) => ReactNode;
}| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
key | string | yes | — | React key of the header cell and the body cell, and the TaskTransformed field read when render is absent |
header | ReactNode | yes | — | Header cell content |
width | number | no | 120 | Column width in px |
render | (task: TaskTransformed) => ReactNode | no | — | Cell content for one task |
render receives a TaskTransformed, not the raw Task, so the derived fields
barLeft, barWidth, depth, order, originalOrder and isSummary are available on it.
The critical-path fields — earlyStart, earlyFinish, lateStart, lateFinish, totalSlack,
freeSlack, critical and duration — are written onto the row only while the criticalPath
prop is on. Without it they are undefined, so a column keyed on one of them renders an empty
cell.
Default columns
With showTaskList on and no columns array, the chart renders these three:
| # | key | header | width | Cell content |
|---|---|---|---|---|
| 0 | name | Name | 220 | no render → String(task.name) |
| 1 | startDate | Start | 110 | dayjs(task.startDate).format('YYYY-MM-DD') |
| 2 | endDate | End | 110 | dayjs(task.endDate).format('YYYY-MM-DD') |
The widths sum to 440, which is the pane's initial width in that case.
The array is internal — it is not exported from the package, so it cannot be imported, spread or
extended. Passing columns replaces it wholesale. The 'YYYY-MM-DD' date format is likewise
module-private: to change it, supply your own startDate / endDate columns with a render.
Cell rendering without render
/** Without a render, task[key] is shown as-is */
function renderCell(column: GanttColumn, task: TaskTransformed): ReactNode {
if (column.render) return column.render(task);
const value = (task as unknown as Record<string, unknown>)[column.key];
return value == null ? "" : String(value);
}task[key] | Cell shows |
|---|---|
null or undefined | "" |
false | "false" |
0 | "0" |
| an array | its comma join |
| an object | "[object Object]" |
| anything else | String(value) |
A key that matches no field on TaskTransformed renders an empty cell rather than throwing.
The first column
Index 0 of the array is the tree column. The chart attaches to it, and only to it:
- the indent span,
16pxperdepthlevel; - the expander button on a summary row or a group header (a
20pxspacer on every other row); - the
+Nbadge when the row carries more than one task, whereNisrow.tasks.length - 1; - a native
titletooltip whose text is alwaystask.name, whatever the column renders.
Widths resolve differently for index 0 as well:
/** The first column is the tree column - it takes the leftover width; the rest keep theirs */
function cellStyle(column: GanttColumn, index: number) {
const width = column.width ?? DEFAULT_COLUMN_WIDTH;
return index === 0
? { flex: `1 1 ${width}px`, minWidth: 60 }
: { flex: `0 0 ${width}px` };
}| Index | Flex | Effect of resizing the pane |
|---|---|---|
0 | 1 1 <width>px, min-width: 60 | absorbs every px the splitter adds or removes, down to 60 |
1..n | 0 0 <width>px | fixed; never grows or shrinks |
DEFAULT_COLUMN_WIDTH is 120 and is not exported from the package. The same cellStyle runs on
the header cells, so header and body stay aligned.
Example
// TaskListColumns.tsx
import { ReactGanttChart } from '@jaeungkim/gantt-chart';
import type { GanttColumn, Task } from '@jaeungkim/gantt-chart';
const columns: GanttColumn[] = [
{ key: 'name', header: 'Task', width: 240 },
{ key: 'depth', header: 'Level', width: 80 },
{
key: 'progress',
header: 'Done',
width: 90,
render: (task) => `${task.progress ?? 0}%`,
},
];
const tasks: Task[] = [
{
id: 'design',
name: 'Design',
startDate: '2026-01-05',
endDate: '2026-01-09',
parentId: null,
sequence: '1',
progress: 100,
},
{
id: 'build',
name: 'Build',
startDate: '2026-01-12',
endDate: '2026-01-23',
parentId: null,
sequence: '2',
progress: 40,
},
];
export default function TaskListColumns() {
return <ReactGanttChart tasks={tasks} columns={columns} />;
}depth has no render; it is read off TaskTransformed and stringified. progress has one, so
the cell shows 40% instead of 40.
Constraints
- Group header rows ignore
columnsentirely. A grouped band renders one full-width cell holding the expander, the group label and the task count. - Columns are not virtualized. Every entry renders for every visible row, so a 30-column task list costs 30 cells per row.
- The pane width is computed once, at mount, from the column widths. Changing
columnsafterwards does not resize the pane, and the splitter clamps it to120–800px regardless of the sum. - There is no column resizing, reordering, hiding, pinning, sorting or filtering.
widthis fixed at render time and the array renders in the order given. - Duplicate
keyvalues produce a React duplicate-key warning —keyidentifies both the header cell and the body cell. columnsdoes nothing without a visible pane, and an explicitshowTaskList={false}beats a suppliedcolumnsarray. SeeGanttProps.