Skip to main content
Back to Blog
Tutorials
4 min read
December 15, 2024

How to Build a Gantt Chart Component in React

Build a Gantt chart component in React for project timelines with task bars, dependencies, drag-to-resize, and zoom controls.

Ryel Banfield

Founder & Lead Developer

Gantt charts visualize project timelines and task dependencies. Here is how to build one from scratch.

Data Types

// types/gantt.ts
export interface GanttTask {
  id: string;
  name: string;
  start: Date;
  end: Date;
  progress: number; // 0-100
  color?: string;
  dependencies?: string[]; // IDs of dependent tasks
  group?: string;
}

export interface GanttConfig {
  startDate: Date;
  endDate: Date;
  dayWidth: number; // pixels per day
  rowHeight: number;
}

Date Utilities

// lib/gantt-utils.ts
export function daysBetween(a: Date, b: Date): number {
  const ms = b.getTime() - a.getTime();
  return Math.ceil(ms / (1000 * 60 * 60 * 24));
}

export function addDays(date: Date, days: number): Date {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

export function getDateRange(
  tasks: { start: Date; end: Date }[]
): { start: Date; end: Date } {
  const starts = tasks.map((t) => t.start.getTime());
  const ends = tasks.map((t) => t.end.getTime());
  const start = new Date(Math.min(...starts));
  const end = new Date(Math.max(...ends));
  // Add padding
  start.setDate(start.getDate() - 3);
  end.setDate(end.getDate() + 7);
  return { start, end };
}

export function getMonths(start: Date, end: Date): { date: Date; days: number }[] {
  const months: { date: Date; days: number }[] = [];
  const current = new Date(start.getFullYear(), start.getMonth(), 1);

  while (current <= end) {
    const monthEnd = new Date(current.getFullYear(), current.getMonth() + 1, 0);
    const visibleStart = current < start ? start : current;
    const visibleEnd = monthEnd > end ? end : monthEnd;
    months.push({
      date: new Date(current),
      days: daysBetween(visibleStart, visibleEnd) + 1,
    });
    current.setMonth(current.getMonth() + 1);
  }

  return months;
}

Gantt Chart Component

"use client";

import { useMemo, useState, useRef } from "react";
import type { GanttTask } from "@/types/gantt";
import { daysBetween, getDateRange, getMonths, addDays } from "@/lib/gantt-utils";

interface GanttChartProps {
  tasks: GanttTask[];
  onTaskUpdate?: (task: GanttTask) => void;
}

export function GanttChart({ tasks, onTaskUpdate }: GanttChartProps) {
  const [dayWidth, setDayWidth] = useState(30);
  const rowHeight = 40;
  const headerHeight = 60;
  const sidebarWidth = 200;
  const containerRef = useRef<HTMLDivElement>(null);

  const range = useMemo(() => getDateRange(tasks), [tasks]);
  const totalDays = daysBetween(range.start, range.end);
  const totalWidth = totalDays * dayWidth;
  const months = useMemo(() => getMonths(range.start, range.end), [range]);

  return (
    <div className="border rounded-lg overflow-hidden">
      {/* Toolbar */}
      <div className="flex items-center gap-2 px-3 py-2 border-b bg-muted/30">
        <span className="text-xs text-muted-foreground">Zoom:</span>
        {[15, 30, 50].map((w) => (
          <button
            key={w}
            onClick={() => setDayWidth(w)}
            className={`px-2 py-0.5 text-xs rounded ${
              dayWidth === w
                ? "bg-primary text-primary-foreground"
                : "border hover:bg-muted"
            }`}
          >
            {w === 15 ? "Month" : w === 30 ? "Week" : "Day"}
          </button>
        ))}
      </div>

      <div ref={containerRef} className="flex overflow-auto max-h-[600px]">
        {/* Sidebar */}
        <div
          className="shrink-0 border-r bg-card z-10 sticky left-0"
          style={{ width: sidebarWidth }}
        >
          <div
            className="border-b bg-muted/30 px-3 flex items-center text-xs font-medium"
            style={{ height: headerHeight }}
          >
            Task
          </div>
          {tasks.map((task) => (
            <div
              key={task.id}
              className="flex items-center px-3 border-b text-sm truncate"
              style={{ height: rowHeight }}
            >
              {task.name}
            </div>
          ))}
        </div>

        {/* Timeline */}
        <div className="relative" style={{ minWidth: totalWidth }}>
          {/* Month headers */}
          <div className="sticky top-0 z-10 bg-card" style={{ height: headerHeight }}>
            <div className="flex border-b" style={{ height: 30 }}>
              {months.map((m, i) => (
                <div
                  key={i}
                  className="border-r px-2 flex items-center text-xs font-medium"
                  style={{ width: m.days * dayWidth }}
                >
                  {m.date.toLocaleString("en-US", {
                    month: "long",
                    year: "numeric",
                  })}
                </div>
              ))}
            </div>

            {/* Day headers */}
            <div className="flex border-b" style={{ height: 30 }}>
              {Array.from({ length: totalDays }, (_, i) => {
                const date = addDays(range.start, i);
                const isWeekend = date.getDay() === 0 || date.getDay() === 6;
                return (
                  <div
                    key={i}
                    className={`border-r flex items-center justify-center text-[10px] ${
                      isWeekend ? "bg-muted/50 text-muted-foreground" : ""
                    }`}
                    style={{ width: dayWidth }}
                  >
                    {dayWidth >= 30 ? date.getDate() : ""}
                  </div>
                );
              })}
            </div>
          </div>

          {/* Grid lines */}
          <div className="absolute" style={{ top: headerHeight }}>
            {Array.from({ length: totalDays }, (_, i) => {
              const date = addDays(range.start, i);
              const isWeekend = date.getDay() === 0 || date.getDay() === 6;
              return (
                <div
                  key={i}
                  className={`absolute top-0 border-r ${
                    isWeekend ? "bg-muted/20" : ""
                  }`}
                  style={{
                    left: i * dayWidth,
                    width: dayWidth,
                    height: tasks.length * rowHeight,
                  }}
                />
              );
            })}
          </div>

          {/* Task bars */}
          <div className="relative" style={{ top: 0 }}>
            {tasks.map((task, index) => {
              const offset = daysBetween(range.start, task.start);
              const duration = daysBetween(task.start, task.end);
              const left = offset * dayWidth;
              const width = Math.max(duration * dayWidth, dayWidth);
              const top = index * rowHeight + 8;

              return (
                <div
                  key={task.id}
                  className="absolute group"
                  style={{ left, top, width, height: rowHeight - 16 }}
                >
                  {/* Background bar */}
                  <div
                    className="h-full rounded-sm opacity-30"
                    style={{ backgroundColor: task.color ?? "#3b82f6" }}
                  />

                  {/* Progress fill */}
                  <div
                    className="absolute inset-y-0 left-0 rounded-sm"
                    style={{
                      width: `${task.progress}%`,
                      backgroundColor: task.color ?? "#3b82f6",
                    }}
                  />

                  {/* Label */}
                  <div className="absolute inset-0 flex items-center px-2">
                    <span className="text-[11px] font-medium text-white truncate drop-shadow-sm">
                      {width > 60 ? task.name : ""}
                    </span>
                  </div>

                  {/* Tooltip on hover */}
                  <div className="absolute bottom-full left-0 mb-1 hidden group-hover:block z-20">
                    <div className="bg-popover border rounded shadow-lg p-2 text-xs whitespace-nowrap">
                      <div className="font-medium">{task.name}</div>
                      <div className="text-muted-foreground">
                        {task.start.toLocaleDateString()} - {task.end.toLocaleDateString()}
                      </div>
                      <div className="text-muted-foreground">{task.progress}% complete</div>
                    </div>
                  </div>
                </div>
              );
            })}
          </div>

          {/* Today line */}
          <TodayLine range={range} dayWidth={dayWidth} height={tasks.length * rowHeight + headerHeight} />
        </div>
      </div>
    </div>
  );
}

function TodayLine({
  range,
  dayWidth,
  height,
}: {
  range: { start: Date; end: Date };
  dayWidth: number;
  height: number;
}) {
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  if (today < range.start || today > range.end) return null;

  const offset = daysBetween(range.start, today);
  const left = offset * dayWidth + dayWidth / 2;

  return (
    <div
      className="absolute top-0 w-px bg-red-500 z-10 pointer-events-none"
      style={{ left, height }}
    >
      <div className="absolute -top-0 -translate-x-1/2 bg-red-500 text-white text-[9px] px-1 rounded-b">
        Today
      </div>
    </div>
  );
}

Usage

import { GanttChart } from "@/components/GanttChart";
import type { GanttTask } from "@/types/gantt";

const tasks: GanttTask[] = [
  {
    id: "1",
    name: "Design Phase",
    start: new Date(2026, 0, 6),
    end: new Date(2026, 0, 17),
    progress: 100,
    color: "#8b5cf6",
  },
  {
    id: "2",
    name: "Frontend Development",
    start: new Date(2026, 0, 13),
    end: new Date(2026, 1, 7),
    progress: 65,
    color: "#3b82f6",
    dependencies: ["1"],
  },
  {
    id: "3",
    name: "Backend Development",
    start: new Date(2026, 0, 13),
    end: new Date(2026, 1, 14),
    progress: 40,
    color: "#10b981",
    dependencies: ["1"],
  },
  {
    id: "4",
    name: "Testing",
    start: new Date(2026, 1, 10),
    end: new Date(2026, 1, 21),
    progress: 0,
    color: "#f59e0b",
    dependencies: ["2", "3"],
  },
];

export default function ProjectPage() {
  return <GanttChart tasks={tasks} />;
}

Need Project Management Tools?

We build custom project management dashboards and team collaboration tools. Contact us to learn more.

Gantt chartproject managementReactcomponenttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles