Today let's build a responsive navbar, an animated burger icon and a beautiful uncovering effect from scratch using HTML, CSS, clip-path, flexbox, grid layout and so much more...
Read the full article or watch me code this on Youtube:
Result
Blank HTML5 Document
Let's start with a blank HTML5 Document. This is our starting point which we're going to extend bit by bit throughout the following chapters.
Did you know that typing an exclamation mark (!) and hitting tab in VS CODE generates a blank HTML5 Document?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Navbar</title>
</head>
<body>
<!-- content will go here -->
</body>
</html>
Using Styles and External Fonts
There are three (more or less) external resources we have to include in the documents <head>
:
- Our own stylesheet in
styles.css
. - We'll be using the font Nunito as the page's default font. Therefore it is loaded via Google Fonts. The font weights 200 and 400 will be used.
- Font Awesome via
cdnjs.com
. Watch out for theintegrity
attribute as it is a nice security feature. If the SHA checksum of the file downloaded by the user agent differs from the SHA checksum given in theintegrity
attribute, the browser knows that the downloaded content was tampered with and is therefore rejected. See this MDN Article for more about subresource integrity.
<head>
....
<!-- or own styles -->
<link rel="stylesheet"
href="styles.css"
type="text/css" />
<!-- "Nunito" font via google fonts -->
<link rel="preconnect"
href="https://fonts.gstatic.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;400&display=swap" >
<!-- Font Awesome via CDNJS -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css"
integrity="sha512-HK5fgLBL+xu6dm/Ii3z4xhlSUyZgTT9tuc/hSrtw6uzJOvgRr2a9jyxxT1ely+B+xFAmJKVSTbpM/CuL7qxO8w=="
crossorigin="anonymous" />
</head>
Variables and Basic Styles
Now, in our styles.css
file, let's start with a few basic things. For instance, variables. It's extremely useful for customization to have the colors we'll be using in variables and common, repetitive things like the transition setup also.
Note the nav-height
variable as it will allow us to control the navigation bar's height and many other elements' position and size are going to depend on that variable.
:root {
--fg-color: rgba(255, 255, 255, 0.9);
--bg-color: #2b2b2b;
--highlight-primary: #008aff;
--gradient:
linear-gradient(300deg, #ba4aff, rgba(186, 74, 255, 0) 70%),
linear-gradient(227deg, #008aff, rgba(0, 138, 255, 0) 70%),
linear-gradient(104deg, #00ffc6, rgba(0, 255, 198, 0) 74%);
--nav-height: 3rem;
--transition: 250ms ease-out;
--transition-long: 500ms ease-out;
}
This block resets padding
, margin
and box-sizing
for each element, so it brings us a great deal of consistency across browsers and elements.
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
On the root <html>
element we're defining the basic text color as well as the basic font, including the font size. This is quite important, as we're going to use a lot of rem
values and they are always relative to the font size being set on the root <html>
element.
html {
font-family: "Nunito", sans-serif;
font-size: 18px;
font-weight: 200;
}
The <body>
is simply going to be a flex container that centers content horizontally and puts content to the very top. min-height: 100vh
ensures that the body always takes at least the full viewport height, which spans the background color across the entire page.
body {
padding-top: var(--nav-height);
min-height: 100vh;
background: var(--bg-color);
color: var(--fg-color);
}
Header Markup & CSS Setup
Let's start with a simple <header>
tag...
<header>
</header>
... which is going to be positioned fixed
, i.e. independently of the scrolling position in the document, it is always going to have the position provided through top
, left
and right
.
header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
font-size: 1.5rem;
background: var(--bg-color);
color: var(--fg-color);
box-shadow: -2px 2px 8px 0px rgb(0 0 0 / 80%);
border-bottom: 1px solid var(--highlight-primary);
z-index: 1;
}
For managing the flow of content inside the header, we'll use a CSS grid that automatically generates a new column for each element through grid-auto-flow: column
. Setting grid-auto-columns
to max-content
tells the CSS grid to fit each cell nicely around the content of each element without enforcing line breaks.
header {
...
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
}
The Logo
On the left there's going to be a logo which is simply a feather icon from font awesome:
<header>
<div class="logo">
<i class="fas fa-feather-alt"></i>
</div>
</header>
The feature will be styled such that:
- It is placed in the center of the containing
div.logo
- Has a background accorindg to the variable
--highlight-primary
- Has its top left and bottom right corner rounded off to make it fit the shape of the feather.
.logo {
display: grid;
place-content: center;
padding: 0rem 1rem;
color: var(--highlight-primary);
}
.logo > i {
border-top-left-radius: 50%;
border-bottom-right-radius: 50%;
padding: 0.25rem;
background: var(--highlight-primary);
color: var(--bg-color);
}
The navigation bar itself
Let's put a few items into our navigation bar. Which means that we're going to use the contextual HTML5 <nav>
tag as a container for the navigation bar. Inside it, an unordered list is used and visually tailred to our needs. The basic structure of each navigation item is a link with an icon and a label.
<header>
...
<nav>
<ul>
<li>
<a href="#"><i class="far fa-chart-bar"></i>Dashboard</a>
</li>
<li>
<a href="#"><i class="far fa-edit"></i>Projects</a>
</li>
<li>
<a href="#"><i class="far fa-envelope-open"></i>Posts</a>
</li>
</ul>
</nav>
</header>
In order to align all navigation items next to each other, the unordered list ul
is also setup to be a grid container just like we already did on the header tag. There are two slight differences:
- The
grid-template-rows
property is set to1fr
, stating that there is only one row and this row should consume all the space that is available. - There is a
gap
between each cell of0.5rem
header ul {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
grid-template-rows: 1fr;
gap: 0.5rem;
padding: 0rem 1.5rem;
list-style-type: none;
}
The li
list item is again set up to be a grid container and by default the only child element (<a>
) is nicely stretched to the items dimensions.
header ul > li {
display: grid;
padding: 0.5rem;
}
Link Styling
Since we want to center the content of each link vertically, it is a flex
container with align-items
set to center
. The rest of the styling is there for
- putting some padding and slightly rounded of corners
- setting the text and background color
- removing the unnecessary underline styling of the link
- making the background color transitionable, which is useful for the hover style.
The icon in each link has a slightly decreased font size in order to achieve a good proportion with respect to the actual text's size.
header a {
display: flex;
align-items: center;
padding: 0rem 1.5rem;
border-radius: 0.25rem;
color: var(--fg-color);
background-color: rgba(0, 0, 0, 0.1);
text-decoration: none;
transition: background-color var(--transition);
}
header a > i {
margin-right: 0.5rem;
color: var(--highlight-primary);
font-size: 1rem;
transition: color var(--transition);
}
So for the hover styling the links background color is changed to the highlight color and the icon's color is is also changed to the darker background color:
header a:hover {
background-color: var(--highlight-primary);
}
header a:hover > i {
color: var(--bg-color);
}
The burger menu button
The menu items will be put into a separate navigation bar attached to the right corner, once the screen gets too small to contain all navigation items. However, then we're going to need some sort of trigger or button that will make the menu visible or invisible if clicked again. For this reason we're going to use a checkbox as we can react to the state of it through the :checked
pseudo-class. And since styling a checkbox can be really messy, we'll be using a <label>
that is referencing the checkbox via the for
attribute. This allows us to put all the styling that we need on the label and since the for
attribute contains the same id as the one given on the checkbox, clicking the label will also toggle the checkbox itself. So we can safely hide the checkbox without losing functionality (Not talking about accessibility at this point - which I want to cover in a seperate post).
<header>
<div class="logo">...</div>
<input type="checkbox" class="toggle" id="nav-toggle">
<label for="nav-toggle" id="nav-toggle-label">
...
</label>
<nav>...</nav>
</header>
So, as indicated above, the label and the checkbox are hidden by default, since we assume by default that there is enough space for the navigation items.
#nav-toggle-label {
display: none;
cursor: pointer;
}
#nav-toggle {
display: none;
}
By using @media
queries we can now determine the state of the viewport which we define as too small to display the navigation items. In our case, if the screen/viewport size is less than 768px
wide, certain styles are applied that change the appearance and behaviour of the navigation bar.
@media screen and (max-width: 768px) {
/*
styles which are applied only if the page is
rendered on a screen (e.g. not printed) and
the viewport's width is less thant 768px wide
*/
}
The first thing we're going to do is to position the nav
element similar to the header in fixed
mode, but not attached to the top edge of the viewport, but to the right:
@media screen and (max-width: 768px) {
header nav {
position: fixed;
top: 0;
bottom: 0;
width: 24rem;
right: 0rem;
padding-top: var(--nav-height);
background: var(--gradient);
box-shadow: -2px 2px 8px 0px rgb(0 0 0 / 80%);
transition: clip-path var(--transition-long),
background-color var(--transition-long);
}
}
Now we also have to change the flow of the grid inside the unordered list ul
via grid-auto-flow
from column
to row
. That way the navigation items become arranged vertically.
@media screen and (max-width: 768px) {
header ul {
grid-auto-flow: row;
grid-template-columns: 1fr;
grid-template-rows: none;
grid-auto-rows: max-content;
gap: 0.5rem;
padding: 0;
}
}
The rest of the styling slightly changes the alignment of the links to left (place-content
) and changes the colors in default and hover state.
@media screen and (max-width: 768px) {
header a {
place-content: flex-start;
padding: 0.5rem 1.5rem;
}
header a > i {
color: var(--bg-color);
}
header a:hover {
background-color: var(--bg-color);
}
header a:hover > i {
color: var(--highlight-primary);
}
}
Menu button
Finally it's time to style the menu button. First, let's configure the header
tag to keep the logo on the left and push the menu button to the right, which is achieved by setting justify-content
to space-between
:
@media screen and (max-width: 768px) {
header {
justify-content: space-between;
align-items: center;
}
}
To create a menu button that is able to transition from being a button with a menu icon (three bars stacked on top of each other) to a button with a close icon, we simply put three div.bars
inside the label
.
<header>
...
<label for="nav-toggle" id="nav-toggle-label">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</label>
...
</header>
So, let's setup a few things:
- The
--size
variable controls the size of the menu button and is proportionally to the nav bar's height - The
--bar-height
variable tells each bar how high it should to be. - The bars are arranged in a stacked way by using flex-box in
column
direction withspace-between
such that all bears are distributed evenly through the containing element's height.
@media screen and (max-width: 768px) {
#nav-toggle-label {
--size: calc(var(--nav-height) / 3);
--bar-height: 2px;
display: flex;
flex-direction: column;
justify-content: space-between;
flex-basis: auto;
width: var(--size);
height: var(--size);
margin-right: calc(var(--nav-height) / 3);
z-index: 2;
}
Each bar itself is simply setup to have the --bg-color
variable as background color and to use the --bar-height
variable from above. With width: 100%;
is is stretched to consume the entire horizontal space being available.
#nav-toggle-label .bar {
display: inline-block;
height: var(--bar-height);
width: 100%;
background-color: var(--bg-color);
transition: transform 250ms ease-out;
}
}
Now it get's interesting as we're going to define how the three bars are reshaped to a close icon. Each selector is bound to the :checked
pseudo-class of the checkbox, so this styles are not applied if the checkbox is not checked.
- The topmost bar is rotated by 225 degrees and shifted down to the center of the containing element.
- The lowest bar is rotated by 135 degrees (225 degrees minus 90 degrees) and shifted upwards to the center of the containing element.
- The middle element is simply made invisible by scaling it to zero on the x-axis.
@media screen and (max-width: 768px) {
#nav-toggle:checked + #nav-toggle-label > .bar:nth-child(1) {
transform:
translate(0, calc(var(--size) / 2 - var(--bar-height) / 2))
rotate(225deg);
}
#nav-toggle:checked + #nav-toggle-label > .bar:nth-child(2) {
transform: scaleX(0);
}
#nav-toggle:checked + #nav-toggle-label > .bar:nth-child(3) {
transform:
translate(0, calc(-1 * var(--size) / 2 + var(--bar-height) / 2))
rotate(135deg);
}
}
Expanding and collapsing the nav menu
We're almost done! Now we need to cut down the navigation bar to a small circle positioned right behind the menu icon if the checkbox is not checked. That's why we use a clip-path
with radius of 1rem
(one third of the navbar's height) and a center point in the top right corner of the viewport but shifted slightly left and down to match the navigation bar's height.
@media screen and (max-width: 768px) {
header nav {
...
clip-path: circle(
calc(var(--nav-height) / 3)
at
calc(100% - var(--nav-height) / 2)
calc(0% + var(--nav-height) / 2)
);
}
}
This circle is then expanded to 125% of the larger edge of the viewport (125vmax
). The vmax
unit is really convenient as it dynamically changes its base measure to either the viewport's width or height - depending on which one of the both is the larger one. So 125% of the larger edge gives us enough space to cover the height of the viewport.
@media screen and (max-width: 768px) {
#nav-toggle:checked + * + nav {
clip-path: circle(125vmax at 100% 0%);
background-color: var(--bg-color);
}
}
As a final touch, the clip-path
property is made transitionable to make expanding and collapsing the menu quite smooth. And that's already it!
@media screen and (max-width: 768px) {
header nav {
...
transition: clip-path var(--transition-long),
background-color var(--transition-long);
}
}