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
donepercentage 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-DDstrings shifts days in some zones; keep UTC.
How to use it
- Save as
components/Gantt.tsx. <Gantt tasks={[{ id: '1', name: 'Design', start: '2025-03-03', end: '2025-03-14' }]} />.- Dates use the
YYYY-MM-DDformat. - Change bar colours through the optional
colorfield 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.