How to build an HTML gantt chart

A gantt chart is a grid where each bar starts and ends on a column. CSS grid draws it without a library, and a share link keeps the schedule current after the dates move.

An HTML gantt chart is a CSS grid with one column per time unit and one bar per task, placed with grid-column. For a schedule that is read rather than dragged around, that is the whole technique.

The alternative most people reach for is a screenshot of a project tool. It is out of date the moment a date moves, and it cuts off at the bottom.

A finished gantt chart page. Week columns across the top, task bars spanning them.
A finished gantt chart page. Week columns across the top, task bars spanning them.

Lay out the grid first

Decide the unit before you write any markup. Days for a two-week launch, weeks for a quarter, months for a roadmap.

Then build one grid with a label column and one equal track per unit.

.gantt {
  display: grid;
  grid-template-columns: 180px repeat(12, 1fr);
  gap: 4px 0;
  align-items: center;
}

Twelve tracks is twelve weeks. The 180px column holds the task names and does not move when the timeline does.

Place the bars

Each task is two elements: the name in column 1, and the bar spanning its date range.

<div class="name">Data migration</div>
<div class="bar" style="grid-column: 4 / 8">Weeks 3-6</div>

Grid columns are one-indexed and the label occupies track 1, so week 3 starts at track 4. The end value is exclusive, which is where almost every gantt layout goes wrong by one column.

Write that rule down next to the markup. Start track equals week number plus one, end track equals last week plus two.

Header row, today line, milestones

The header is the same grid with a cell per unit, so labels sit exactly above the bars they describe.

A today marker is one absolutely positioned line inside a relatively positioned grid. Its left offset is the fraction of the timeline already elapsed.

Milestones are zero-width bars. Give the cell a single grid track and render a diamond with a rotated square, so it reads as a point rather than a duration.

The chart with a vertical today line crossing the bars and a diamond milestone marker.
The chart with a vertical today line crossing the bars and a diamond milestone marker.

What to use, by what you need

What you need CSS grid gantt Charting library Project tool screenshot
Read-only schedule for stakeholders Fits Heavier than needed Goes stale
Dependency arrows between tasks Needs SVG work Built in Built in
Drag a bar to change a date No Built in Built in
Opens with no login Yes Yes No
Still correct next week Yes, if you edit the page Yes No
Prints cleanly Yes, with a print rule Varies Usually cropped

Most schedules that get shared are read-only. The reader wants to know when their part starts, not to reorganise the plan.

Colour by status, not by team

A gantt chart with eight team colours is decoration. A gantt chart with three status colours answers a question.

  • Done. Muted fill, no border. It is context, not news.
  • In progress. Solid fill, the strongest colour on the page.
  • Not started. Outline only, no fill.
  • At risk. One accent colour, used nowhere else on the page.

Keep the count at four or fewer and add a legend row above the grid. If you support dark mode, check the muted fill has not disappeared into the background there.

Narrow screens

A twelve-week grid at readable size is wider than a phone. There are two honest answers and one bad one.

The bad one is shrinking the whole grid until the labels are unreadable. Do not scale a timeline to fit.

Instead, wrap the grid in a container with overflow-x: auto and give the grid a minimum width, so the timeline scrolls sideways inside the page. Or below the breakpoint, swap to a plain list of task, start date, end date.

Either way the viewport meta tag has to be present, or the phone renders the desktop width and shrinks it.

A schedule changes. That is what schedules do, and it is the reason a gantt chart should live at an address rather than in an attachment.

  1. Open the file in the HTML file opener to confirm it renders outside your folder.
  2. Paste the HTML into a NOS document. The grid, the colours and any script render as written.
  3. Share, then Share link, then Create link. Leave it unlisted for an internal plan.
  4. Put the link in the project channel. Reference the same link in every status update.
The share dialog, link created, Public on the web left unticked.
The share dialog, link created, Public on the web left unticked.

When a milestone slips, click the bar label in the document and change it. The link you sent in week one still points at the current schedule in week nine.

That is the difference between this and a screenshot of a live page, which freezes the moment you took it.

Where a gantt chart is the wrong picture

Not every schedule is a gantt. Two common mismatches.

If the tasks have no duration, only dates, you want a timeline. Turning a timeline into a link covers that shape.

If the question is how much of each thing, not when, you want a bar chart instead.

A bar label being corrected by clicking the text in the document. The address is unchanged.
A bar label being corrected by clicking the text in the document. The address is unchanged.

Before you send it

  • Does the first bar line up with the header cell you think it does? Check the off-by-one.
  • Is there a <title>, so the tab and preview card are not blank?
  • Does the timeline scroll rather than shrink on a narrow window?
  • Are the status colours distinguishable without relying on hue alone?
  • Is the link unlisted if the plan names clients or unannounced dates?

Questions people ask

Can you build a gantt chart with only HTML and CSS?

Yes. CSS grid gives you one column per time unit, and each task bar is placed with grid-column start and end. No JavaScript is needed for a static schedule. You only need scripting for dragging bars or recalculating dependencies.

How do I map dates to grid columns?

Pick the smallest unit you actually report on, usually a week. Number the columns from the project start, then a task running weeks 3 to 6 is grid-column 3 / 7. The end value is exclusive, which is the most common off-by-one in this layout.

How do I share a gantt chart so the dates stay current?

Paste the HTML into a NOS document and send the share link. When a milestone slips you edit the bar in place and the same address shows the new schedule. Sending the file instead means every slip produces another copy in someone's inbox.

Does a gantt chart work on a phone?

Partly. A twenty-week timeline cannot fit a phone screen at readable size. Wrap the grid in a horizontally scrolling container and keep the task name column pinned, or offer a stacked list of task, start, and end below the breakpoint.

Keep reading