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.

View Demo Project

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

PropTypeDescription
tasksTaskInput[]Array of tasks to render. Sent to the chart as series only when it changed (compared deeply)
optionsOmit<GanttUserOptions, 'series'>Full chart configuration (excludes series, which is derived from tasks)
widthstring | numberChart width, overrides options.width
heightstring | numberChart height, overrides options.height
pixelsPerDaynumberTimeline 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' | stringColor 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)
classNamestringCSS class on the wrapper <div>
styleCSSPropertiesInline styles on the wrapper <div>
refRef<ApexGanttHandle>Imperative handle, see Imperative API via ref
on*(detail) => voidOne 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.

PropCore eventDetail typeFires when
onTaskUpdatetaskUpdateTaskUpdateEventDetailAn edit is being committed, before taskUpdateSuccess
onTaskUpdateSuccesstaskUpdateSuccessTaskUpdateSuccessEventDetailAn edit was applied
onTaskUpdateErrortaskUpdateErrorTaskUpdateErrorEventDetailAn edit failed to apply
onTaskValidationErrortaskValidationErrorTaskValidationErrorEventDetailThe task edit dialog failed validation
onTaskAddedtaskAddedTaskAddedEventDetailA task was inserted
onTaskDeletedtaskDeletedTaskDeletedEventDetailA task was removed (once per task in a cascading delete)
onTaskMovedtaskMovedTaskMovedEventDetailA task was moved to a new parent
onDependencyAddeddependencyAddedDependencyChangeEventDetailA dependency link was created, by addDependency() or drawn by the user
onDependencyRemoveddependencyRemovedDependencyChangeEventDetailA dependency link was removed, including once per link a deleted task takes with it
onTaskDraggedtaskDraggedTaskDraggedEventDetailA bar was dropped at a new date
onTaskResizedtaskResizedTaskResizedEventDetailA bar was resized by one of its handles
onTaskProgressChangedtaskProgressChangedTaskProgressChangedEventDetailThe in-bar progress handle was dragged to a new value
onSelectionChangeselectionChangeSelectionChangeEventDetailThe set of selected rows changed (needs enableSelection)
onDependencyArrowUpdatedependencyArrowUpdateDependencyArrowUpdateDetailInternal 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
onHistoryChangehistoryChangeHistoryChangeEventDetailThe undo/redo stack changed
onSortChangesortChangeSortChangeEventDetailThe active sort changed
onFilterChangefilterChangeFilterChangeEventDetailThe active filter changed
onGroupChangegroupChangeGroupChangeEventDetailThe active grouping changed
onColumnResizecolumnResizeColumnResizeEventDetailA task-list column was resized
onColumnReordercolumnReorderColumnReorderEventDetailTask-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:

MethodDescription
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"]