@jaeungkim/gantt-chart

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;
}
FieldTypeRequiredDefaultMeaning
keystringyesReact key of the header cell and the body cell, and the TaskTransformed field read when render is absent
headerReactNodeyesHeader cell content
widthnumberno120Column width in px
render(task: TaskTransformed) => ReactNodenoCell 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:

#keyheaderwidthCell content
0nameName220no renderString(task.name)
1startDateStart110dayjs(task.startDate).format('YYYY-MM-DD')
2endDateEnd110dayjs(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 arrayits comma join
an object"[object Object]"
anything elseString(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, 16px per depth level;
  • the expander button on a summary row or a group header (a 20px spacer on every other row);
  • the +N badge when the row carries more than one task, where N is row.tasks.length - 1;
  • a native title tooltip whose text is always task.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` };
}
IndexFlexEffect of resizing the pane
01 1 <width>px, min-width: 60absorbs every px the splitter adds or removes, down to 60
1..n0 0 <width>pxfixed; 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 columns entirely. 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 columns afterwards does not resize the pane, and the splitter clamps it to 120800 px regardless of the sum.
  • There is no column resizing, reordering, hiding, pinning, sorting or filtering. width is fixed at render time and the array renders in the order given.
  • Duplicate key values produce a React duplicate-key warning — key identifies both the header cell and the body cell.
  • columns does nothing without a visible pane, and an explicit showTaskList={false} beats a supplied columns array. See GanttProps.

On this page