Dependencies

ApexGantt draws dependency arrows between tasks to represent scheduling relationships: a line from one task bar to another that indicates which task must happen first, or when two tasks must align. ApexGantt supports four dependency types, validates automatically against circular references, and exposes a programmatic API for adding and removing edges at runtime.

Dependency types

Every dependency connects two tasks: a predecessor (the task that constrains) and a successor (the task that is constrained). The four types control which ends of the bars are connected.

TypeNameMeaningWhen to use
'FS'Finish-to-StartThe predecessor must finish before the successor can start.Sequential work: design then development, testing then deployment. This covers 80%+ of real project dependencies and is the default when no type is specified.
'FF'Finish-to-FinishBoth tasks must finish at the same time.Parallel work where two streams must complete together: writing and editing a document, or two review cycles that close on the same date.
'SS'Start-to-StartBoth tasks must start at the same time.Work that launches together: client kickoff and internal planning, or two parallel builds that must begin in lockstep.
'SF'Start-to-FinishThe predecessor must start before the successor can finish.Rarely used in standard projects. Models hand-off scenarios where a follow-on task keeps running until its predecessor has started (for example, a standby system that stays active until the replacement is live).

Declaring dependencies in data

The simplest way to add dependencies is directly on the task object using the dependency field. Set it to the id of the predecessor task. ApexGantt renders the arrow automatically when the chart first renders.

import ApexGantt from 'apexgantt'

ApexGantt.setLicense('YOUR-LICENSE-KEY')

const gantt = new ApexGantt(document.getElementById('gantt'), {
  series: [
    {
      id: 'task-1',
      name: 'Design',
      startTime: '2024-03-01',
      endTime: '2024-03-15',
    },
    {
      id: 'task-2',
      name: 'Development',
      startTime: '2024-03-16',
      endTime: '2024-03-31',
      dependency: 'task-1',   // FS by default: task-1 must finish before task-2 starts
    },
    {
      id: 'task-3',
      name: 'Testing',
      startTime: '2024-04-01',
      endTime: '2024-04-10',
      dependency: ['task-1', 'task-2'],  // multiple predecessors: both must finish first
    },
  ],
})

gantt.render()

When dependency is a plain string, the type is 'FS'. When it is an array, every listed predecessor also defaults to 'FS'. To declare a non-FS type, use the programmatic API described in the next section.

Adding dependencies programmatically

Use gantt.addDependency(fromId, toId) to connect two tasks after the chart has rendered. This is the right approach when dependencies come from user interaction, a form, or data loaded after initial render.

// Basic FS dependency: task-1 must finish before task-2 starts
gantt.addDependency('task-1', 'task-2')

Pass a third argument to set the type and an optional lag in days. Lag shifts the successor's constraint forward in time without changing the predecessor's dates.

// Finish-to-Finish with a 2-day lag: both finish together, but task-2 gets 2 extra days
gantt.addDependency('task-1', 'task-2', { type: 'FF', lag: 2 })

// Start-to-Start: both must start together
gantt.addDependency('task-3', 'task-4', { type: 'SS' })

// Start-to-Finish
gantt.addDependency('task-5', 'task-6', { type: 'SF' })

addDependency throws if either task ID does not exist, or if the edge already exists. Use canAddDependency (described in the next section) to guard the call when the task IDs are not guaranteed to be valid.

Both addDependency and removeDependency are recorded in undo history, so the user can undo them with the chart's built-in undo/redo controls.

Safe add workflow

The recommended pattern for user-driven dependency creation is to check validity first, then add:

const fromId = 'task-1'
const toId   = 'task-3'

const result = gantt.canAddDependency(fromId, toId)

if (result.ok) {
  gantt.addDependency(fromId, toId)
} else {
  // Show the user why the dependency cannot be created
  console.warn('Cannot add dependency:', result.reason)
}

Removing dependencies

removeDependency(fromId, toId) removes the arrow between two tasks. Like addDependency, it is recorded in undo history.

gantt.removeDependency('task-1', 'task-2')

Validation and cycle detection

ApexGantt prevents invalid dependency edges automatically. Before any edge is created, the library checks for several rejection conditions. You can run these checks yourself ahead of time with canAddDependency, which returns { ok: true } on success or { ok: false, reason: string } on failure.

const result = gantt.canAddDependency(fromId, toId)

if (!result.ok) {
  switch (result.reason) {
    case 'self':
      // fromId === toId. A task cannot depend on itself.
      break

    case 'task-missing':
      // One of the IDs does not exist in the current series.
      break

    case 'duplicate':
      // An edge from fromId to toId already exists.
      break

    case 'cycle':
      // Adding this edge would create a circular dependency chain,
      // for example A→B→C→A. ApexGantt rejects cycles because they
      // make scheduling impossible: no task could start or finish.
      break

    case 'summary-descendant':
      // One task is an ancestor or descendant of the other in the task
      // hierarchy (via parentId). Dependencies between parent and child
      // rows produce ambiguous rendering and are not allowed.
      break

    case 'hook-veto':
      // A beforeDependencyChange hook returned false, canceling the add.
      // See the hook section below for details.
      break
  }
}

Cycle detection is the most important check. Without it, a cycle such as "Testing depends on Development, Development depends on Design, Design depends on Testing" would make it impossible to compute start dates, draw the critical path, or determine task order. ApexGantt runs a graph traversal to detect the cycle before committing the edge, so the chart data always represents a valid directed acyclic graph.

Critical path

Enable critical path highlighting by setting enableCriticalPath: true. ApexGantt computes the longest path through the dependency network and identifies which tasks have zero float: any delay to a task on the critical path pushes out the overall project end date.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  enableCriticalPath: true,
  criticalBarColor: '#e53935',    // color for task bars on the critical path
  criticalArrowColor: '#e53935',  // color for dependency arrows on the critical path

  series: [
    { id: 'task-1', name: 'Design',      startTime: '2024-03-01', endTime: '2024-03-15' },
    { id: 'task-2', name: 'Development', startTime: '2024-03-16', endTime: '2024-03-31', dependency: 'task-1' },
    { id: 'task-3', name: 'QA Review',   startTime: '2024-03-20', endTime: '2024-04-05', dependency: 'task-1' },
    { id: 'task-4', name: 'Launch',      startTime: '2024-04-06', endTime: '2024-04-07', dependency: ['task-2', 'task-3'] },
  ],
})

gantt.render()

In the example above, the path Design → Development → Launch is longer than Design → QA Review → Launch, so Design, Development, and Launch are on the critical path. QA Review has float (it could slip a few days without delaying Launch) and is therefore not highlighted.

Float is the amount of time a task can slip before it affects the project end date. Tasks with zero float are critical. Tasks with positive float are not.

Styling arrows

Color

arrowColor sets the default color for all dependency arrows. criticalArrowColor overrides that color for arrows that lie on the critical path when enableCriticalPath is true.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  arrowColor: '#94A3B8',
  criticalArrowColor: '#e53935',
  enableCriticalPath: true,
  series: [ /* ... */ ],
})

Corner radius and hit area

The dependencies object controls the arrow geometry.

cornerRadius rounds the corners of the right-angle bends in the arrow path. The default is 4. Setting it to 0 gives sharp 90-degree corners; higher values produce rounded elbows.

hitWidth adds an invisible wider hit area around each arrow, making it easier for users to hover or click thin arrows. The default is 0 (no extra area). Setting it to 10 adds 10 pixels of invisible area on each side.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  dependencies: {
    cornerRadius: 8,   // rounder arrow joints
    hitWidth: 10,      // easier to hover/click arrows
  },
  series: [ /* ... */ ],
})

Custom CSS classes per arrow

classBuilder receives a context object for each dependency arrow and returns a CSS class string. Use this to apply different visual styles per dependency type or per task.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  dependencies: {
    classBuilder: (ctx) => {
      if (ctx.type === 'FF') return 'ff-arrow'
      if (ctx.type === 'SS') return 'ss-arrow'
      return ''
    },
  },
  series: [ /* ... */ ],
})

Then define the classes in your stylesheet:

.ff-arrow {
  stroke: #6366f1;
  stroke-dasharray: 4 3;
}

.ss-arrow {
  stroke: #10b981;
}

Custom arrow tooltip

dependencies.tooltipTemplate lets you replace the built-in dependency tooltip with custom HTML. The function receives a context object with from and to task objects and the dependency type.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  dependencies: {
    tooltipTemplate: (ctx) => `
      <div style="padding: 6px 10px; font-size: 13px;">
        <strong>${ctx.from.name}</strong>
        &rarr;
        <strong>${ctx.to.name}</strong>
        <span style="margin-left: 8px; color: #64748b;">(${ctx.type})</span>
      </div>
    `,
  },
  series: [ /* ... */ ],
})

The template string is inserted as HTML, so you can include any markup. Keep tooltips concise: they appear on hover and disappear when the pointer moves away.

beforeDependencyChange hook

beforeDependencyChange runs before any dependency is added or removed, including changes made through the UI by the user. Return false to cancel the change.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  beforeDependencyChange: ({ fromId, toId, action, type }) => {
    // action: 'add' | 'remove'
    // type: the DependencyType string ('FS', 'FF', 'SS', 'SF')

    // Example: prevent adding dependencies that cross phase boundaries
    const fromPhase = getPhase(fromId)
    const toPhase   = getPhase(toId)

    if (action === 'add' && fromPhase !== toPhase) {
      console.warn(`Cross-phase dependency blocked: ${fromId}${toId}`)
      return false   // cancels the add
    }

    // Return nothing (or true) to allow the change
  },

  series: [ /* ... */ ],
})

When the hook returns false, canAddDependency reports the reason as 'hook-veto'. The hook fires for both programmatic calls (addDependency, removeDependency) and interactive drag-to-connect gestures in the UI.

Use this hook for business rules that go beyond what cycle detection and duplicate checking already cover: enforcing phase constraints, requiring approval workflows, logging audit trails, or keeping a separate data store in sync.

Interactive dependency creation

When enableTaskDrag: true is set, hovering near the edge of a task bar reveals a connector handle. Users can drag from the handle to another task bar to create a new dependency in the UI. The beforeDependencyChange hook fires for these gestures, so the same validation logic applies whether a dependency is created in code or by a user.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  enableTaskDrag: true,

  beforeDependencyChange: ({ fromId, toId, action }) => {
    // Called for both UI and programmatic changes
    if (action === 'add') {
      return confirm(`Add dependency from ${fromId} to ${toId}?`)
    }
  },

  series: [ /* ... */ ],
})