DEV3 Technologies home

Gantt bars from task dates

Convert task start and end dates into percentage-based timeline bars with week gridlines in a small React component.

React / Next.jsChartsgantttimelinedatesreact

Live preview

Code

export type Task = { id: string; name: string; start: string; end: string; color?: string };

const DAY = 864e5;
// Parse as UTC so daylight-saving changes never alter the day count.
const utc = (iso: string) => {
  const [y, m, d] = iso.split('-').map(Number) as [number, number, number];
  return Date.UTC(y, m - 1, d);
};

export function bars(tasks: Task[]) {
  if (!tasks.length) return { items: [], mondays: [] as number[], days: 0 };
  const starts = tasks.map((t) => utc(t.start));
  const ends = tasks.map((t) => utc(t.end));
  const min = Math.min(...starts);
  const days = Math.round((Math.max(...ends) - min) / DAY) + 1; // end date is inclusive

  const items = tasks.map((t, i) => {
    const len = Math.round((ends[i]! - starts[i]!) / DAY) + 1;
    if (len < 1) throw new Error(`Task "${t.name}" ends before it starts`);
    return { ...t, left: (Math.round((starts[i]! - min) / DAY) / days) * 100, width: (len / days) * 100 };
  });

  const mondays: number[] = [];
  for (let d = 0; d < days; d++) if (new Date(min + d * DAY).getUTCDay() === 1) mondays.push((d / days) * 100);
  return { items, mondays, days };
}

export function Gantt({ tasks }: { tasks: Task[] }) {
  const { items, mondays } = bars(tasks);
  return (
    <div className="text-sm" role="table" aria-label="Project timeline">
      {items.map((t) => (
        <div key={t.id} role="row" className="grid grid-cols-[8rem_1fr] items-center border-b">
          <div role="rowheader" className="truncate pr-2">{t.name}</div>
          <div role="cell" className="relative h-9">
            {mondays.map((m) => (
              <span key={m} className="absolute inset-y-0 w-px bg-slate-200" style={{ left: `${m}%` }} />
            ))}
            <span
              title={`${t.name}: ${t.start} to ${t.end}`}
              aria-label={`${t.start} to ${t.end}`}
              className="absolute top-2 h-5 rounded"
              style={{ left: `${t.left}%`, width: `${t.width}%`, background: t.color ?? '#2563eb' }}
            />
          </div>
        </div>
      ))}
    </div>
  );
}

How it works

What it solves

A Gantt chart looks complicated, but the core is a single mapping: a date range becomes a horizontal position and width. Once that is solid, you can render the chart with plain divs, with no charting library and with layout that scales to any container width.

How it works

Dates are parsed as UTC midnight with Date.UTC, which avoids daylight-saving shifts that would otherwise make a day 23 or 25 hours long. The helper dayIndex returns whole days since the earliest start. The timeline range runs from the first start to the last end, and each bar's left is its start offset divided by the total span, width is its inclusive length divided by the span, both as percentages. End dates are inclusive, so a one-day task still has a visible width of one day.

Vertical gridlines mark each Monday: a CSS repeating-linear-gradient is not accurate across months, so the component computes the Monday offsets and renders absolutely positioned lines at the same percentages. Each row has a label column and a track; the bar carries a title and aria-label with the dates so the information is not only visual.

Percentage positioning means the chart resizes with its container and prints cleanly.

Adapting it

  • Add progress by overlaying a darker child bar with a done percentage width.
  • Draw dependency arrows with an SVG layer using the same percentages.
  • Zoom by wrapping the track in a horizontally scrollable element with a fixed pixel width.

Pitfalls

  • Always validate that end is not before start.
  • Local-time parsing of YYYY-MM-DD strings shifts days in some zones; keep UTC.

How to use it

  1. Save as components/Gantt.tsx.
  2. <Gantt tasks={[{ id: '1', name: 'Design', start: '2025-03-03', end: '2025-03-14' }]} />.
  3. Dates use the YYYY-MM-DD format.
  4. Change bar colours through the optional color field on each task.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets