React ApexGantt
Installation
npm install react-apexgantt apexgantt
apexgantt is a peer dependency. This page describes the react-apexgantt release built for apexgantt 3.20.0, which requires apexgantt 3.20.0 or later; notes mark what differs on earlier versions.
License Setup
ApexGantt is included in the Premium plan and above, and there is no free tier. Without a key the chart renders with a trial watermark. Set your license key once at application start; your entry file is the best place:
// main.tsx
import React from "react"
import ReactDOM from "react-dom/client"
import { setApexGanttLicense } from "react-apexgantt"
import App from "./App"
setApexGanttLicense("your-license-key-here")
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
A key set later still removes the watermark from charts already on the page (apexgantt 3.17.0 and later); setting it first avoids a one-frame watermark. From apexgantt 3.20.0 the key's plan must be Premium or higher.
Quick Start
import { ApexGanttChart } from "react-apexgantt"
import type { TaskInput } from "react-apexgantt"
const tasks: TaskInput[] = [
{
id: "phase-1",
name: "Phase 1: Research",
startTime: "2024-03-01",
endTime: "2024-03-15",
progress: 100,
},
{
id: "task-1",
name: "User interviews",
startTime: "2024-03-01",
endTime: "2024-03-08",
parentId: "phase-1",
progress: 100,
},
{
id: "task-2",
name: "Competitive analysis",
startTime: "2024-03-06",
endTime: "2024-03-15",
parentId: "phase-1",
progress: 80,
},
{
id: "phase-2",
name: "Phase 2: Design",
startTime: "2024-03-16",
endTime: "2024-04-05",
progress: 30,
dependency: "phase-1",
},
]
// the dates above are ISO, so tell the chart (the default is 'MM-DD-YYYY')
const options = { inputDateFormat: "YYYY-MM-DD" }
export default function App() {
return <ApexGanttChart tasks={tasks} options={options} pixelsPerDay={25.7} height="500px" />
}
parentId links a task to its parent row to form a hierarchy; the parent draws a summary bar spanning its children. dependency draws an arrow from the predecessor: a plain id is a finish-to-start (FS) link, and { taskId, type, lag } sets another type ('SS', 'FF' or 'SF') or a lag. Links are drawn and used by the critical path; they do not move tasks.
Props
| Prop | Type | Description |
|---|---|---|
tasks | TaskInput[] | Array of tasks to render. Sent to the chart as series only when it changed (compared deeply) |
options | Omit<GanttUserOptions, 'series'> | Full chart configuration (excludes series, which is derived from tasks) |
width | string | number | Chart width, overrides options.width |
height | string | number | Chart height, overrides options.height |
pixelsPerDay | number | Timeline zoom, in pixels-per-day. The header tier is auto-picked (reference points: 0.5 year, 1.6 quarter, 4.9 month, 25.7 week, 80 day). Omit to auto-fit the data span. Overrides options.pixelsPerDay |
theme | 'light' | 'dark' | string | Color theme, overrides options.theme. With apexgantt 3.21.0 and later it also takes the name of a registered theme. Leave it out and the chart is light, or follows the dark variant of an Ignite UI theme (see Using with Ignite UI); unsetting it sends theme: undefined, which, with apexgantt 3.21.0 and later, returns the chart to no theme (light, or the Ignite UI dark variant) |
className | string | CSS class on the wrapper <div> |
style | CSSProperties | Inline styles on the wrapper <div> |
ref | Ref<ApexGanttHandle> | Imperative handle, see Imperative API via ref |
on* | (detail) => void | One callback per chart event, see Events |
How prop changes reach the chart
On every render the component compares the props with the ones the chart last received and calls the core update() with only what changed: series only when tasks changed (or options.parsing, which reads it), and each other prop or options key only when its own value changed. A key you remove from options is sent as undefined, which resets it to its default. When nothing changed, nothing is sent.
The comparison is deep for arrays and plain objects, so a new array with the same contents is not a change. Functions compare by identity: an inline tooltipTemplate, column render or beforeTaskUpdate is a new function on every render and is resent each time. Define callbacks at module scope or with useCallback, and build an options object that holds them with useMemo.
With apexgantt 3.20.0, an update() without series keeps the edits made in the chart (drags, resizes, dialog and inline edits, updateTask, addTask, deleteTask, moveTask, dependency changes). A changed tasks array replaces the chart's data and clears its undo history, so change tasks when you mean to load different data, and read the current state with getInstance()?.getTasks(). To save the chart, save getInstance()?.getTasks() together with getInstance()?.getDependencies() (both apexgantt 3.20.0): a task's dependency holds only the link it was loaded with, so links added or drawn later are only in getDependencies(), which also reflects undo and redo. See Saving links for reloading them. Earlier wrapper releases resent every prop whenever any prop changed, and earlier apexgantt versions rebuilt the tasks from series on every update(), so edits made in the chart were lost on any re-render that changed a prop.
Events
Every event the chart dispatches has a callback prop, named on plus the event name. Each callback receives the event's typed detail. The component listens only to the events you pass a callback for and reads the callback when the event fires, so an inline arrow function neither re-subscribes nor updates the chart. Earlier wrapper releases exposed eight of these: onTaskUpdate, onTaskUpdateSuccess, onTaskUpdateError, onTaskValidationError, onTaskDragged, onTaskResized, onSelectionChange and onDependencyArrowUpdate.
| Prop | Core event | Detail type | Fires when |
|---|---|---|---|
onTaskUpdate | taskUpdate | TaskUpdateEventDetail | An edit is being committed, before taskUpdateSuccess |
onTaskUpdateSuccess | taskUpdateSuccess | TaskUpdateSuccessEventDetail | An edit was applied |
onTaskUpdateError | taskUpdateError | TaskUpdateErrorEventDetail | An edit failed to apply |
onTaskValidationError | taskValidationError | TaskValidationErrorEventDetail | The task edit dialog failed validation |
onTaskAdded | taskAdded | TaskAddedEventDetail | A task was inserted |
onTaskDeleted | taskDeleted | TaskDeletedEventDetail | A task was removed (once per task in a cascading delete) |
onTaskMoved | taskMoved | TaskMovedEventDetail | A task was moved to a new parent |
onDependencyAdded | dependencyAdded | DependencyChangeEventDetail | A dependency link was created, by addDependency() or drawn by the user |
onDependencyRemoved | dependencyRemoved | DependencyChangeEventDetail | A dependency link was removed, including once per link a deleted task takes with it |
onTaskDragged | taskDragged | TaskDraggedEventDetail | A bar was dropped at a new date |
onTaskResized | taskResized | TaskResizedEventDetail | A bar was resized by one of its handles |
onTaskProgressChanged | taskProgressChanged | TaskProgressChangedEventDetail | The in-bar progress handle was dragged to a new value |
onSelectionChange | selectionChange | SelectionChangeEventDetail | The set of selected rows changed (needs enableSelection) |
onDependencyArrowUpdate | dependencyArrowUpdate | DependencyArrowUpdateDetail | Internal redraw signal: an arrow was recomputed because a linked task moved, on every frame of a drag, for this chart only. Use onDependencyAdded / onDependencyRemoved for link changes. Earlier releases of the component never called it |
onHistoryChange | historyChange | HistoryChangeEventDetail | The undo/redo stack changed |
onSortChange | sortChange | SortChangeEventDetail | The active sort changed |
onFilterChange | filterChange | FilterChangeEventDetail | The active filter changed |
onGroupChange | groupChange | GroupChangeEventDetail | The active grouping changed |
onColumnResize | columnResize | ColumnResizeEventDetail | A task-list column was resized |
onColumnReorder | columnReorder | ColumnReorderEventDetail | Task-list columns were reordered |
With apexgantt 3.20.0, taskUpdate and taskUpdateSuccess fire once per committed edit on every path (drag, resize, progress drag, dialog, inline edit, keyboard, updateTask), with a source field naming the path, and taskDragged, taskResized and taskProgressChanged fire only after beforeTaskUpdate accepts the edit. An app that saves on both onTaskDragged and onTaskUpdateSuccess therefore saves each drag twice: save on onTaskUpdateSuccess alone. When a parent bar is dragged or moved with the keyboard, each moved task gets its own events, but all of the gesture's taskUpdate events arrive before its taskUpdateSuccess events.
import { ApexGanttChart } from "react-apexgantt"
export default function GanttWithEvents() {
return (
<ApexGanttChart
tasks={tasks}
height="500px"
onTaskUpdateSuccess={(detail) => {
console.log("Task updated:", detail.taskId, detail.source)
// save to your backend
}}
onTaskAdded={(detail) => {
console.log("Task added:", detail.task)
}}
onTaskUpdateError={(detail) => {
console.error("Update failed:", detail.error)
}}
/>
)
}
Imperative API via ref
Use useRef<ApexGanttHandle> to call chart methods programmatically after mount.
import { useRef } from "react"
import { ApexGanttChart } from "react-apexgantt"
import type { ApexGanttHandle } from "react-apexgantt"
export default function GanttWithControls() {
const ganttRef = useRef<ApexGanttHandle>(null)
return (
<div>
<div style={{ marginBottom: 12, display: "flex", gap: 8 }}>
<button onClick={() => ganttRef.current?.zoomIn()}>Zoom In</button>
<button onClick={() => ganttRef.current?.zoomOut()}>Zoom Out</button>
<button onClick={() => {
ganttRef.current?.updateTask("task-1", { progress: 100 })
}}>
Mark task-1 done
</button>
</div>
<ApexGanttChart ref={ganttRef} tasks={tasks} height="500px" />
</div>
)
}
Available methods on ApexGanttHandle:
| Method | Description |
|---|---|
update(options) | Merge Partial<GanttUserOptions> into the chart and re-render. Pass only the keys to change; with apexgantt 3.20.0 leaving out series keeps the edits made in the chart |
updateTask(taskId, data) | Update a single task by ID with partial fields |
zoomIn() | Zoom in one step (the zoom is multiplied by 1.4) |
zoomOut() | Zoom out one step |
destroy() | Destroy the chart and clean up DOM and listeners. Prop changes are no longer forwarded; remount the component for a new chart |
getInstance() | Return the underlying ApexGantt instance, or null before mount and after destroy() |
Every other core method is on getInstance(): scrollToTask, addTask, deleteTask, moveTask, addDependency, undo, redo, sort, filter, groupBy, getSelectedTasks, exportChart and the rest, plus getTask, getTasks, getDependencies, expandTask and collapseTask from apexgantt 3.20.0. See the Methods reference.
ganttRef.current?.getInstance()?.scrollToTask("task-2")
Dynamic zoom and theme
Bind pixelsPerDay and theme to React state. The component compares props on every render and calls update() internally with only the props that changed.
With apexgantt 3.20.0 a theme change returns the colours the theme owns (barBackgroundColor, rowBackgroundColors, fontColor and the rest listed under update()) to the new theme's values, except the ones you set in options, which keep your values. Every other option carries over.
import { useState } from "react"
import { ApexGanttChart } from "react-apexgantt"
export default function DynamicGantt() {
const [pixelsPerDay, setPixelsPerDay] = useState(25.7)
const [theme, setTheme] = useState<"light" | "dark">("light")
return (
<div>
<div style={{ marginBottom: 12, display: "flex", gap: 12 }}>
<label>
Zoom:
<select
value={pixelsPerDay}
onChange={(e) => setPixelsPerDay(Number(e.target.value))}
>
<option value={80}>Day</option>
<option value={25.7}>Week</option>
<option value={4.9}>Month</option>
<option value={1.6}>Quarter</option>
<option value={0.5}>Year</option>
</select>
</label>
<button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
Toggle theme
</button>
</div>
<ApexGanttChart
tasks={tasks}
pixelsPerDay={pixelsPerDay}
theme={theme}
height="500px"
/>
</div>
)
}
Options
Pass any configuration from the options reference via the options prop. The series key is omitted: it is always derived from tasks. Keep the object stable (module scope, as here, or useMemo) when it holds functions, so a re-render does not resend them.
import type { GanttUserOptions } from "react-apexgantt"
const options: Omit<GanttUserOptions, "series"> = {
inputDateFormat: "YYYY-MM-DD",
enableTaskDrag: true,
enableTaskResize: true,
enableTaskEdit: true,
enableInlineEdit: true,
barBackgroundColor: "#537CFA",
rowHeight: 36,
columnConfig: [
{ key: "name", title: "Task", minWidth: "160px", flexGrow: 3 },
{ key: "startTime", title: "Start", minWidth: "100px", flexGrow: 1 },
{ key: "endTime", title: "End", minWidth: "100px", flexGrow: 1 },
{ key: "progress", title: "Progress", minWidth: "80px", flexGrow: 1 },
],
}
export default function ConfiguredGantt() {
return <ApexGanttChart tasks={tasks} options={options} height="600px" />
}
Localization
Locale options go through options like any other option. A locale pack (apexgantt 3.22.0) is plain data, so it needs no wrapper release:
import { ja } from 'apexgantt/locales/ja'
// <ApexGanttChart tasks={tasks} options={{ locale: { pack: ja } }} />
Every message in a pack is a string, so the options compare equal from one render to the next. A timelineHeaderFormat function, by contrast, is a new function each time it is written inline; define it once outside the component. See Localization and RTL for packs, locale.tag, the message keys and right-to-left layout.
Using with Ignite UI
From apexgantt 3.21.0 the chart can follow an Ignite UI theme loaded on the page (bars, text, gridlines, the header, the tooltip, the font, and light or dark). Import the add-on once, before the first chart renders:
import 'apexgantt/ignite-ui'
It needs no extra licence key. Leave the theme prop unset so the theme's dark variant can apply: a theme you pass, even 'light', wins. See Using with Ignite UI.
useGanttData: parsing external data
When your API returns data in a different shape, useGanttData maps and memoizes it into the TaskInput[] format ApexGantt expects. It only re-parses when data or parsing changes.
import { useGanttData, ApexGanttChart } from "react-apexgantt"
const rawApiData = [
{
task_id: "T1",
task_name: "Planning Phase",
start: "2024-01-01",
end: "2024-01-10",
completion: 100,
},
{
task_id: "T2",
task_name: "Development",
start: "2024-01-11",
end: "2024-01-25",
completion: 60,
depends_on: "T1",
},
{
task_id: "T3",
task_name: "Frontend",
start: "2024-01-11",
end: "2024-01-18",
completion: 80,
parent_task: "T2",
},
]
const parsingConfig = {
id: "task_id",
name: "task_name",
startTime: "start",
endTime: "end",
progress: "completion",
dependency: "depends_on",
parentId: "parent_task",
}
export default function ParsedGantt() {
const tasks = useGanttData({ data: rawApiData, parsing: parsingConfig })
return <ApexGanttChart tasks={tasks} options={{ inputDateFormat: "YYYY-MM-DD" }} height="500px" />
}
The result is memoised on data and parsing, so keep both stable (state, useMemo or module scope) to avoid parsing on every render. With apexgantt 3.20.0 the parsing config can also map showSummaryBar, baseline, segments, assignees and barTextColor.
For nested objects, use dot-notation paths. For values that need transformation, pass an object with key and transform:
const parsingConfig = {
id: "project.task.id",
name: "project.task.title",
startTime: "project.dates.start",
endTime: "project.dates.end",
progress: {
key: "project.status.completion",
transform: (value: number) => value * 100, // convert 0-1 to 0-100
},
}
useGanttEvents: memoized event handlers
<ApexGanttChart> already reads its callbacks when an event fires, so you do not need this hook to avoid re-subscribing. Use useGanttEvents when the same handlers also go to memoised children or effect dependencies, where a stable identity matters. It accepts any of the on* props from the Events table and returns the same keys, with identities that stay the same across renders, ready to spread onto the chart:
import { useGanttEvents, ApexGanttChart } from "react-apexgantt"
export default function GanttWithMemoizedEvents() {
const events = useGanttEvents({
onTaskUpdateSuccess: (detail) => console.log("saved:", detail.taskId),
onTaskDragged: (detail) => console.log("dragged:", detail),
onHistoryChange: (detail) => console.log("can undo:", detail.canUndo),
})
return <ApexGanttChart tasks={tasks} height="500px" {...events} />
}
The hook still returns handleTaskUpdate, handleTaskDragged and handleTaskResized, the names earlier releases used; they are deprecated in favour of the on* keys.
TypeScript
All types are exported from react-apexgantt, including the detail type of every event:
import type {
TaskInput,
ThemeMode,
GanttUserOptions,
ApexGanttHandle,
ApexGanttChartProps,
GanttEventHandlers,
TaskUpdateSuccessEventDetail,
HistoryChangeEventDetail,
} from "react-apexgantt"
TaskType and Orientation are enums, so they are exported as values and can be used at runtime (type: TaskType.Milestone). Earlier wrapper releases exported TaskType as a type only, which compiled import { TaskType } but failed at runtime; import it from apexgantt on those versions. With apexgantt 3.20.0 the plain string type: 'milestone' also compiles.
The event map is available too, from either package:
import type { GanttEventMap } from "apexgantt"
type TaskUpdateDetail = GanttEventMap["taskUpdateSuccess"]["detail"]