How to Build a Simple Gantt Chart With CSS and JavaScript

So far in our series of CSS chart tutorials, we’ve learned how to create different types of charts including bar charts, thermometer charts, and pie charts.
Today we’ll continue this journey by building and presenting data in a Gantt chart. Unlike the other chart tutorials, we’ll be making heavy use of JavaScript to implement various aspects of the chart. You’ll be able to use this basis as a Gantt chart template for future projects.
Note: keep in mind that it’s perfectly possible to build a pure CSS Gantt chart thanks to CSS Grid Layout. See how it’s done in this tutorial!
The Gantt Chart We’re Building
Here’s the chart we’ll be creating (hit rerun to see it animate). It shows a series of tasks, making clear when those tasks are scheduled to begin along the course of a week–either at the start of a day, or halfway through–and when they’re due to be completed:
What is a Gantt Chart?
Developed by mechanical engineer Henry Gantt in the run up to WW1, Gantt charts were originally used to manage the logistics of mobilizing American soldiers and munitions. Nowadays Gantt’s techniques are used for way more than warfare; in fact you’ll find them present in most industries.They make it easy to visualize a list of tasks, the dependance of those tasks on one another, and their state of completion.
“A Gantt chart is a type of bar chart that illustrates a project schedule. This chart lists the tasks to be performed on the vertical axis, and time intervals on the horizontal axis. The width of the horizontal bars in the graph shows the duration of each activity.” – Wikipedia



Gantt charts give managers the ability to manage projects, schedule tasks, watch progress, delegate responsibilities, and allocate resources.
In actual fact, you don’t have to be a manager to use them. Anyone who wants to organize their tasks can benefit from this form of project visualization.
Regardless of using it as a management tool, Gantt’s layout can also have other uses. For example, you can use this approach within a corporate site as a timeline for visualizing the company’s history.
Gantt Solutions
Most project management platforms use Gantt charts as a core part of their offering. ClickUp, Zapier, and Monday.com are all names you’ll hear when project management apps are discussed (though the founders at Monday.com stress their timeline “focuses on people, not tasks or projects”).
Gantt Chart Templates on Envato Elements
Keynote, PowerPoint, and other presentation apps are more than capable of creating Gantt charts for demonstration purposes. Take a look at some of the Gantt chart templates available right now on Envato Elements.






There are also a number of free and proprietary solutions available out there for building your own Gantt charts. You can create one with Microsoft Excel, Google Sheets, a web app like TeamGantt, a JavaScript library like Highcharts, or even by writing your own code.
In this tutorial we’re going for the latter; let’s create our own simple Gantt chart using CSS and JavaScript!
1. Specify the Page Markup
We’ll begin by defining a wrapper element which contains two lists:
- The first list defines the chart range (x-axis data). In our case, this will contain the days of the week. Each day will represent normal working hours.
- The second list sets the chart data (y-axis data). In our case, the data will include the tasks that need to be performed within a week. Each list item, which describes a task, comes with two custom attributes: the
data-duration
attribute and thedata-color
attribute. The first attribute defines the task duration, while the second one its background color. The value of thedata-duration
attribute should be in the[startDay]-[endDay]
format. That being said, we’ll use thedata-duration="tue-wed"
for a task that should start on Tuesday and finish on Wednesday. Furthermore, a task can also start and finish in the middle of a day. In such a case, thedata-duration
attribute value should be in the[startDay]½-[endDay]½
format. So for example, we’ll use thedata-duration="tue-wed½"
for a task that should start on Tuesday and finish in the middle of Wednesday. Similarly, thedata-duration="tue½-tue"
will describe a task that should start in the middle of Tuesday and finish on the same day.
Here’s the required markup:
1 |
<div class="chart-wrapper"> |
2 |
<ul class="chart-values"> |
3 |
<li>sun</li> |
4 |
<li>mon</li> |
5 |
<li>tue</li> |
6 |
<li>wed</li> |
7 |
<li>thu</li> |
8 |
<li>fri</li> |
9 |
<li>sat</li> |
10 |
</ul>
|
11 |
<ul class="chart-bars"> |
12 |
<li data-duration="tue-wed" data-color="#b03532">Task</li> |
13 |
<li data-duration="wed-sat" data-color="#33a8a5">Task</li> |
14 |
... |
15 |
</ul>
|
16 |
</div>
|
2. Style the Chart
For the sake of simplicity, I won’t walk through the initial reset styles, but feel free to look at them by clicking at the CSS tab of the demo project.
The chart wrapper will have a maximum width with horizontally centered content:
1 |
.chart-wrapper { |
2 |
max-width: 1150px; |
3 |
padding: 0 10px; |
4 |
margin: 0 auto; |
5 |
}
|
The x-axis
The .chart-values
list will be a flex container. Its flex items (days) will be equally distributed across the main axis and have a minimum width of 80px. As a result of that minimum width, on small screens the chart won’t shrink beyond that, and a horizontal scrollbar will appear. Feel free to remove it, if you don’t like this behavior.
To better visualize the left and right boundaries of each item, we’ll use their ::before
pseudo-element. We’ll give it a big hardcoded height (510px) which ensures that it will expand to all tasks. Instead of hardcoding that value, there’s always the option to dynamically calculate it through JavaScript. But let’s skip that solution for now, as it’s of secondary importance.



The corresponding styles:
1 |
:root { |
2 |
--divider: lightgrey; |
3 |
}
|
4 |
|
5 |
.chart-wrapper .chart-values { |
6 |
position: relative; |
7 |
display: flex; |
8 |
margin-bottom: 20px; |
9 |
font-weight: bold; |
10 |
font-size: 1.2rem; |
11 |
}
|
12 |
|
13 |
.chart-wrapper .chart-values li { |
14 |
flex: 1; |
15 |
min-width: 80px; |
16 |
text-align: center; |
17 |
}
|
18 |
|
19 |
.chart-wrapper .chart-values li:not(:last-child) { |
20 |
position: relative; |
21 |
}
|
22 |
|
23 |
.chart-wrapper .chart-values li:not(:last-child)::before { |
24 |
content: ''; |
25 |
position: absolute; |
26 |
right: 0; |
27 |
height: 510px; |
28 |
border-right: 1px solid var(--divider); |
29 |
}
|
The y-axis
The items (bars) of the second list will initially be hidden. Specifically, they will have width: 0
and opacity: 0
. Plus, we’ll give them position: relative
. Later we’ll dynamically set their left
position according to the value of their data-duration
attribute.
Here are the related styles:
1 |
:root { |
2 |
--white: #fff; |
3 |
}
|
4 |
|
5 |
.chart-wrapper .chart-bars li { |
6 |
position: relative; |
7 |
color: var(--white); |
8 |
margin-bottom: 15px; |
9 |
font-size: 16px; |
10 |
border-radius: 20px; |
11 |
padding: 10px 20px; |
12 |
width: 0; |
13 |
opacity: 0; |
14 |
transition: all 0.65s linear 0.2s; |
15 |
}
|
16 |
|
17 |
@media screen and (max-width: 600px) { |
18 |
.chart-wrapper .chart-bars li { |
19 |
padding: 10px; |
20 |
}
|
21 |
}
|
3. Add the JavaScript
When the page loads, or the browser window gets resized, the createChart
function will be executed:
1 |
window.addEventListener("load", createChart); |
2 |
window.addEventListener("resize", createChart); |
Note: As I’ve mentioned in other tutorials, there are different ways for limiting the resize
events that are emitted. For example, one effective solution is to use Lodash’s _.debounce
function. That’s beyond the scope of this tutorial though.
Inside this function, we first do the following things:
- Grab the items of the two lists.
- Convert the
days
NodeList into a real array by using the spread operator. Alternatively, we could have used theArray.from()
method. This conversion will allow us to take advantage of thefilter()
method, which is available in arrays, for filtering the days. - Loop through the tasks.
1 |
function createChart(e) { |
2 |
// 1
|
3 |
const days = document.querySelectorAll(".chart-values li"); |
4 |
const tasks = document.querySelectorAll(".chart-bars li"); |
5 |
// 2
|
6 |
const daysArray = [...days]; |
7 |
// 3
|
8 |
tasks.forEach(el => { |
9 |
...
|
10 |
});
|
11 |
}
|
Loop the Tasks
Next, for each task:
- We grab the value of its
data-duration
attribute (e.g. tue-wed). In addition, we split this value by using the "-" as a separator. - The first string of the returned array represents the starting day of the task (e.g. tue), while the second one its ending day (e.g. wed).
- Knowing its starting day, we filter the
daysArray
to retrieve the list item (day) that matches this day. During this test, we ignore possible presence of the "½" character. Then, we do a few calculations to calculate the requiredleft
property value of the related task. - Knowing its ending day, we filter the
daysArray
to retrieve the list item (day) that matches this day. During this test, we ignore possible presence of the "½" character. Then, we do a few calculations to calculate the requiredwidth
property value of the related task.
1 |
tasks.forEach(el => { |
2 |
// 1
|
3 |
const duration = el.dataset.duration.split("-"); |
4 |
// 2
|
5 |
const startDay = duration[0]; |
6 |
const endDay = duration[1]; |
7 |
let left = 0, |
8 |
width = 0; |
9 |
|
10 |
// 3
|
11 |
if (startDay.endsWith("½")) { |
12 |
const filteredArray = daysArray.filter(day => day.textContent == startDay.slice(0, -1)); |
13 |
left = filteredArray[0].offsetLeft + filteredArray[0].offsetWidth / 2; |
14 |
} else { |
15 |
const filteredArray = daysArray.filter(day => day.textContent == startDay); |
16 |
left = filteredArray[0].offsetLeft; |
17 |
}
|
18 |
|
19 |
// 4
|
20 |
if (endDay.endsWith("½")) { |
21 |
const filteredArray = daysArray.filter(day => day.textContent == endDay.slice(0, -1)); |
22 |
width = filteredArray[0].offsetLeft + filteredArray[0].offsetWidth / 2 - left; |
23 |
} else { |
24 |
const filteredArray = daysArray.filter(day => day.textContent == endDay); |
25 |
width = filteredArray[0].offsetLeft + filteredArray[0].offsetWidth - left; |
26 |
}
|
27 |
...
|
28 |
});
|
Note: In the code above, we use the Element.offsetLeft
property to retrieve the left position of an element relative to its parent. Plus, we take advantage of the Element.offsetWidth
property to find the element’s width. As another option to grab its width, we could have equally used the more precise Element.getBoundingClientRect()
method.
Set Styles
Having calculated the left
and width
values of each task, the last step is to perform the following actions:
- Apply the corresponding styles.
- Grab the value of the
data-color
attribute and set it as the background color of the task. - Show the task. Remember all tasks are initially hidden.
- The actions 2 and 3 should run only on page load as their values won’t change each time we resize the browser window.
1 |
tasks.forEach(el => { |
2 |
...
|
3 |
// 1
|
4 |
el.style.left = `${left}px`; |
5 |
el.style.width = `${width}px`; |
6 |
// 4
|
7 |
if (e.type == "load") { |
8 |
// 2
|
9 |
el.style.backgroundColor = el.dataset.color; |
10 |
// 3
|
11 |
el.style.opacity = 1; |
12 |
}
|
13 |
});
|
Conclusion
That’s it! We managed to build a fully functional Gantt chart. Thanks for following along folks, I hope this provided a good exercise for refreshing your JavaScript skills. Here’s one more look at the final demo:
Play with it and if you find any way to enhance its functionality, be sure to share! Stay tuned for the equivalent CSS Gantt chart.
As always, thanks a lot for reading!
Next Steps
If you want to challenge yourselves, try to convert this vertical timeline into a Gantt chart.
Read More
Spreadsheets, data visualization, Gantt chart templates, graphs, animation–there’s plenty more to dig into!
- How to Make Flowcharts & Gantt Charts in Keynote With TemplatesAndrew Childress26 Oct 2020
- How to Make a Gantt Chart in ExcelBob Flisser09 Mar 2016
- How to Build an Animated CSS Thermometer ChartGeorge Martsoukos19 Jun 2019
- Build a Static Portfolio With Advanced CSS Bar ChartGeorge Martsoukos11 Jun 2019