DEV Community

loading...
Cover image for Let's build a responsive navbar from scratch

Let's build a responsive navbar from scratch

crayoncode
All about coding (and occasionally other topics) and explaining it through beautiful visualizations.
・10 min read

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>
Enter fullscreen mode Exit fullscreen mode

Using Styles and External Fonts

There are three (more or less) external resources we have to include in the documents <head>:

  1. Our own stylesheet in styles.css.
  2. 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.
  3. Font Awesome via cdnjs.com. Watch out for the integrity 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 the integrity 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>
Enter fullscreen mode Exit fullscreen mode

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; 
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Header Markup & CSS Setup

Let's start with a simple <header> tag...

<header>
</header>
Enter fullscreen mode Exit fullscreen mode

... 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

  1. The grid-template-rows property is set to 1fr, stating that there is only one row and this row should consume all the space that is available.
  2. There is a gap between each cell of 0.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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
  */
}
Enter fullscreen mode Exit fullscreen mode

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

  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 with space-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;
  }
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  }
}
Enter fullscreen mode Exit fullscreen mode

Discussion (1)

Collapse
alnahian2003 profile image
Al Nahian

Very Interesting!

Thank you for the detailed explanation and code.