1. Web Design
  2. HTML/CSS
  3. JavaScript for Designers

How to Build a Static Portfolio Page With CSS & JavaScript

Scroll to top
Read Time: 10 min

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:

The page layoutThe page layoutThe page layout

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 setting height: 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.

Using sudo element to output the amount of knowledge

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 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

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.