How to Build a Static Portfolio Page With CSS & JavaScript
In this tutorial we’ll use all the power of flexbox and learn to build a simple, yet attractive static HTML portfolio page. We’ll also create a responsive column chart without using any external JavaScript library, SVG, or the canvas element. Just with plain CSS!
Here’s the project that we’re going to create (click the Skills link in the top corner):
Note: This tutorial assumes some flexbox knowledge. If you’re just beginning, Anna Monus has a great collection of primers to get you started:
1. Begin With the Page Markup
The page markup is pretty straightforward; a header, a heading, a mailto
link and a section:
1 |
<body class="position-fixed d-flex flex-column text-white bg-red"> |
2 |
<header class="page-header"> |
3 |
<nav class="d-flex justify-content-between"> |
4 |
<a href="" class="logo">...</a> |
5 |
<ul class="d-flex"> |
6 |
<li>
|
7 |
<a href="">...</a> |
8 |
</li>
|
9 |
<!-- possibly more list items here -->
|
10 |
</ul>
|
11 |
</nav>
|
12 |
</header>
|
13 |
<h1 class="position-absolute w-100 text-center heading">...</h1> |
14 |
<a class="position-absolute contact" href="">...</a> |
15 |
<section class="position-absolute d-flex align-items-center justify-content-center text-black bg-white skills-section" data-slideIn="to-top"> |
16 |
<!-- content here -->
|
17 |
</section>
|
18 |
</body>
|
Inside the section, we put a close button and a wrapper element with two lists. These lists are responsible for building the column chart:
1 |
<section class="position-absolute d-flex align-items-center justify-content-center text-black bg-white skills-section" data-slideIn="to-top"> |
2 |
<button class="position-absolute skills-close" aria-label="Close Skills Section">✕</button> |
3 |
<div class="d-flex chart-wrapper"> |
4 |
<ul class="chart-levels"> |
5 |
<li>Expert</li> |
6 |
<li>Advanced</li> |
7 |
<li>Intermediate</li> |
8 |
<li>Beginner</li> |
9 |
<li>Novice</li> |
10 |
</ul>
|
11 |
<ul class="d-flex justify-content-around align-items-end flex-grow-1 text-center bg-black chart-skills"> |
12 |
<li class="position-relative bg-red" data-height="80%"> |
13 |
<span class="position-absolute w-100">CSS</span> |
14 |
</li>
|
15 |
<li class="position-relative bg-red" data-height="60%"> |
16 |
<span class="position-absolute w-100">HTML</span> |
17 |
</li>
|
18 |
<li class="position-relative bg-red" data-height="68%"> |
19 |
<span class="position-absolute w-100">JavaScript</span> |
20 |
</li>
|
21 |
<li class="position-relative bg-red" data-height="52%"> |
22 |
<span class="position-absolute w-100">Python</span> |
23 |
</li>
|
24 |
<li class="position-relative bg-red" data-height="42%"> |
25 |
<span class="position-absolute w-100">Ruby</span> |
26 |
</li>
|
27 |
</ul>
|
28 |
</div>
|
29 |
</section>
|
Note: Beyond the elements’ specific classes, our markup contains a number of utility (helper) classes. We’ll use this methodology to keep our CSS as DRY as possible. However, for readability reasons, within the CSS we won’t group common CSS rules.
2. Define Some Basic Styles
Following what we’ve just discussed above, we now specify some reset rules along with a number of helper classes:
1 |
:root { |
2 |
--black: #1a1a1a; |
3 |
--white: #fff; |
4 |
--red: #e21838; |
5 |
--transition-delay: 0.85s; |
6 |
--transition-delay-step: 0.3s; |
7 |
}
|
8 |
|
9 |
* { |
10 |
padding: 0; |
11 |
margin: 0; |
12 |
}
|
13 |
|
14 |
ul { |
15 |
list-style: none; |
16 |
}
|
17 |
|
18 |
a { |
19 |
text-decoration: none; |
20 |
color: inherit; |
21 |
}
|
22 |
|
23 |
button { |
24 |
background: none; |
25 |
border: none; |
26 |
cursor: pointer; |
27 |
outline: none; |
28 |
}
|
29 |
|
30 |
.d-flex { |
31 |
display: flex; |
32 |
}
|
33 |
|
34 |
.flex-column { |
35 |
flex-direction: column; |
36 |
}
|
37 |
|
38 |
.justify-content-center { |
39 |
justify-content: center; |
40 |
}
|
41 |
|
42 |
.justify-content-between { |
43 |
justify-content: space-between; |
44 |
}
|
45 |
|
46 |
.justify-content-around { |
47 |
justify-content: space-around; |
48 |
}
|
49 |
|
50 |
.align-items-center { |
51 |
align-items: center; |
52 |
}
|
53 |
|
54 |
.align-items-end { |
55 |
align-items: flex-end; |
56 |
}
|
57 |
|
58 |
.flex-grow-1 { |
59 |
flex-grow: 1; |
60 |
}
|
61 |
|
62 |
.w-100 { |
63 |
width: 100%; |
64 |
}
|
65 |
|
66 |
.position-relative { |
67 |
position: relative; |
68 |
}
|
69 |
|
70 |
.position-fixed { |
71 |
position: fixed; |
72 |
}
|
73 |
|
74 |
.position-absolute { |
75 |
position: absolute; |
76 |
}
|
77 |
|
78 |
.text-center { |
79 |
text-align: center; |
80 |
}
|
81 |
|
82 |
.text-black { |
83 |
color: var(--black); |
84 |
}
|
85 |
|
86 |
.text-white { |
87 |
color: var(--white); |
88 |
}
|
89 |
|
90 |
.bg-black { |
91 |
background: var(--black); |
92 |
}
|
93 |
|
94 |
.bg-white { |
95 |
background: var(--white); |
96 |
}
|
97 |
|
98 |
.bg-red { |
99 |
background: var(--red); |
100 |
}
|
The naming conventions for our helper classes are inspired by Bootstrap 4’s class names.
3. Style the Page Layout
The page layout will be as simple as this:



Here are the design requirements:
- The page should be full-screen.
- The logo is placed to the top left of the page, the menu to the top right, and the
mailto
link to the bottom right. - The heading is horizontally and vertically centered.
- The section which contains the chart is initially hidden (off-screen).
Here are the corresponding styles to get all that done:
1 |
body { |
2 |
top: 0; |
3 |
right: 0; |
4 |
bottom: 0; |
5 |
left: 0; |
6 |
font: 1rem/1.5 "Montserrat", sans-serif; |
7 |
overflow: hidden; |
8 |
}
|
9 |
|
10 |
.page-header { |
11 |
padding: 20px; |
12 |
border-bottom: 1px solid #e93451; |
13 |
}
|
14 |
|
15 |
.page-header li:not(:last-child) { |
16 |
margin-right: 20px; |
17 |
}
|
18 |
|
19 |
.page-header .logo { |
20 |
font-size: 1.2rem; |
21 |
z-index: 1; |
22 |
transition: color 0.3s; |
23 |
}
|
24 |
|
25 |
.window-opened .page-header .logo { |
26 |
color: var(--black); |
27 |
transition-delay: 0.8s; |
28 |
}
|
29 |
|
30 |
.heading { |
31 |
top: 50%; |
32 |
left: 50%; |
33 |
transform: translate(-50%, -50%); |
34 |
font-size: 2.5rem; |
35 |
}
|
36 |
|
37 |
.contact { |
38 |
bottom: 20px; |
39 |
right: 20px; |
40 |
}
|
41 |
|
42 |
.skills-section { |
43 |
top: 0; |
44 |
right: 0; |
45 |
bottom: 0; |
46 |
left: 0; |
47 |
transform: translateX(100%); |
48 |
}
|
Progress so Far
Here’s what we've built so far!
4. Toggling the Section
Initially, as discussed above, the section is hidden. It will become visible with a nice slide-in effect each time we click on the menu link. We’ll do this by adding the window-opened
class to the body
element and altering the CSS as needed. In the same way, the section will disappear as soon as we click on the close button.
As a bonus, we’ll give ourselves the ability to set the direction of the slide-in animation. We can pass the data-slideIn
custom attribute to the section which will determine the starting position of its animation. Possible attribute values are to-top
, to-bottom
, and to-right
. By default, the section appears from right to left.
Here are the associated styles:
1 |
.skills-section { |
2 |
top: 0; |
3 |
right: 0; |
4 |
bottom: 0; |
5 |
left: 0; |
6 |
transform: translateX(100%); |
7 |
transition: transform 1s; |
8 |
}
|
9 |
|
10 |
.window-opened .skills-section { |
11 |
transform: none; |
12 |
}
|
13 |
|
14 |
[data-slideIn="to-top"] { |
15 |
transform: translateY(100%); |
16 |
}
|
17 |
|
18 |
[data-slideIn="to-bottom"] { |
19 |
transform: translateY(-100%); |
20 |
}
|
21 |
|
22 |
[data-slideIn="to-right"] { |
23 |
transform: translateX(-100%); |
24 |
}
|
And the JavaScript code needed for toggling its state:
1 |
const skillsLink = document.querySelector(".page-header li:nth-child(1) a"); |
2 |
const skillsClose = document.querySelector(".skills-close"); |
3 |
const windowOpened = "window-opened"; |
4 |
|
5 |
skillsLink.addEventListener("click", (e) => { |
6 |
e.preventDefault(); |
7 |
document.body.classList.toggle(windowOpened); |
8 |
});
|
9 |
|
10 |
skillsClose.addEventListener("click", () => { |
11 |
document.body.classList.toggle(windowOpened); |
12 |
});
|
5. Style the Chart
At this point, we’ll have a closer look at the contents of our section. First we have the close button located to the top right side of the section.
Here’s its markup:
1 |
<button class="position-absolute skills-close" aria-label="Close Skills Section">✕</button> |
And its styles:
1 |
.skills-close { |
2 |
top: 20px; |
3 |
right: 20px; |
4 |
font-size: 2rem; |
5 |
}
|
Next we have the chart itself. Let’s also revisit its structure:
1 |
<div class="d-flex chart-wrapper"> |
2 |
<ul class="chart-levels"> |
3 |
<li>Expert</li> |
4 |
... |
5 |
</ul>
|
6 |
|
7 |
<ul class="d-flex justify-content-around align-items-end flex-grow-1 text-center bg-black chart-skills"> |
8 |
<li class="position-relative bg-red" data-height="80%"> |
9 |
<span class="position-absolute w-100">CSS</span> |
10 |
</li>
|
11 |
... |
12 |
</ul>
|
13 |
</div>
|
Here are the key points regarding this markup:
- We set the
.chart-wrapper
element as a flex container with two lists as flex items. - The second list, which measures the knowledge for a certain skill, is itself a flex container. We give it
flex-grow: 1
to grow and take up all the available space.
The initial CSS rules for our chart:
1 |
.chart-wrapper { |
2 |
width: calc(100% - 40px); |
3 |
max-width: 500px; |
4 |
}
|
5 |
|
6 |
.chart-levels li { |
7 |
padding: 15px; |
8 |
}
|
At this point we’ll have a closer look at the items of the second list.
Things to remember:
- They all have a width of 12%. We evenly distribute them across the main axis by giving
justify-content: space-around
to the parent list. - They should sit at the bottom of their container, and thus we set
align-items: flex-end
to the parent list. - Their initial height is 0. As soon as the section becomes visible, their height is animated and receives a value equal to the value of their
data-height
attribute. Just keep in mind that we have to rewrite the desired height values in our CSS because settingheight: attr(data-height)
doesn’t work :(
Here are the related styles:
1 |
:root { |
2 |
...
|
3 |
--transition-delay: 0.85s; |
4 |
--transition-delay-step: 0.3s; |
5 |
}
|
6 |
|
7 |
.chart-skills li { |
8 |
width: 12%; |
9 |
height: 0; |
10 |
border-top-left-radius: 10px; |
11 |
border-top-right-radius: 10px; |
12 |
transition: height 0.65s cubic-bezier(0.51, 0.91, 0.24, 1.16); |
13 |
}
|
14 |
|
15 |
.window-opened .chart-skills li:nth-child(1) { |
16 |
height: 80%; |
17 |
transition-delay: var(--transition-delay); |
18 |
}
|
19 |
|
20 |
.window-opened .chart-skills li:nth-child(2) { |
21 |
height: 60%; |
22 |
transition-delay: calc( |
23 |
var(--transition-delay) + var(--transition-delay-step) |
24 |
);
|
25 |
}
|
26 |
|
27 |
.window-opened .chart-skills li:nth-child(3) { |
28 |
height: 68%; |
29 |
transition-delay: calc( |
30 |
var(--transition-delay) + var(--transition-delay-step) * 2 |
31 |
);
|
32 |
}
|
33 |
|
34 |
.window-opened .chart-skills li:nth-child(4) { |
35 |
height: 52%; |
36 |
transition-delay: calc( |
37 |
var(--transition-delay) + var(--transition-delay-step) * 3 |
38 |
);
|
39 |
}
|
40 |
|
41 |
.window-opened .chart-skills li:nth-child(5) { |
42 |
height: 42%; |
43 |
transition-delay: calc( |
44 |
var(--transition-delay) + var(--transition-delay-step) * 4 |
45 |
);
|
46 |
}
|
As you can see from the code above, we use the transition-delay
and transition-delay-step
CSS variables along with the calc()
CSS function to control the speed of the transition effects. Microsoft Edge doesn’t support those math operations though, so if you need to support it, just pass some static values instead, like this:
1 |
.window-opened .chart-skills li:nth-child(2) { |
2 |
transition-delay: 1.15s; |
3 |
}
|
4 |
|
5 |
.window-opened .chart-skills li:nth-child(3) { |
6 |
transition-delay: 1.45s; |
7 |
}
|
To output the amount of knowledge stated for a certain technology, we’ll use the ::before
pseudo-element.

This value is extracted from the data-height
attribute which is assigned to the items of the second list.
Here are the styles which do this job:
1 |
.chart-skills li::before { |
2 |
content: attr(data-height); |
3 |
position: absolute; |
4 |
top: 10px; |
5 |
left: 0; |
6 |
width: 100%; |
7 |
font-size: 0.85rem; |
8 |
color: var(--white); |
9 |
}
|
Lastly, we add some styles to the span
element which is located inside each of the list items. This element behaves as a label and stores the technology names.

The corresponding styles:
1 |
.chart-skills span { |
2 |
bottom: 0; |
3 |
left: 0; |
4 |
transform: translateY(40px) rotate(45deg); |
5 |
}
|
6. Go Responsive
We’re almost done! As a last thing, let’s ensure that the page has a solid appearance across all screens. We’ll apply two rules specifically for narrow screens:
1 |
@media screen and (max-width: 600px) { |
2 |
html { |
3 |
font-size: 12px; |
4 |
}
|
5 |
|
6 |
.chart-levels li { |
7 |
padding: 15px 10px 15px 0; |
8 |
}
|
9 |
}
|
One important note here is that in our styles we’ve used rem
for setting the font sizes. This approach is really useful because the font sizes aren’t absolute and their values depend on the value of the root element. So, if we decrease the font size of the root element like in the code above, the rem-related font sizes will be dynamically decreased. Nice job folks!
The final state of our project:
7. Browser Support
The demo works well in all recent browsers and devices.
As already discussed earlier, Microsoft Edge still doesn’t support math operations with custom properties inside the calc()
function. To overcome this issue, you’ll need to use hard-coded values instead.
Conclusion
In this tutorial, we improved our flexbox skills by building an attractive static portfolio page. We even challenged ourselves by learning to create a responsive column chart without using any external JavaScript library, SVG, or the canvas
element. Just with plain CSS!
Hopefully you enjoyed this tutorial as much as I enjoyed writing it, and you’ll use this demo as inspiration for developing your own portfolio site. I’d love to see your work–be sure to share it with us!
Further Reading
- How to Build a Full-Screen Responsive Page With FlexboxGeorge Martsoukos20 Nov 2018
- How to Build a Semi-Circle Donut Chart With CSSGeorge Martsoukos25 Aug 2016
- 29 Best Portfolio WordPress Themes for CreativesBrenda Barron19 Jan 2022
- 25+ Best Personal Premium and Free WordPress Portfolio Themes for 2022Brenda Barron29 Nov 2021