Let's build a responsive navbar from scratch
Tags: html, css, webdev, ui
Published: 2021-02-12 15:15:30

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:

{% youtube 8BoVfb5LbH8 %}

# Result

{% codepen https://codepen.io/crayon-code/pen/QWGNJPK %}



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

```html
<!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>`:

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](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) for more about _subresource integrity_.

```html
<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.

```css
: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.

```css
* {
  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.

```css

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.

```css

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

```html
<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`.


```css
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.

```css
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:

```html
<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.

```css
.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.


```html
<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:

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`


```css
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.

```scss
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.


```css
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:

```css
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).

```html
<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.

```css
#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.

```css
@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:

```css
@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.

```css
@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.


```css
@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`:


```css
@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`.

```html
<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 with `space-between` such that all bears are distributed evenly through the containing element's height.


```css
@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.

```css
  #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.


```css
@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.

```css
@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.

```css
@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!


```css
@media screen and (max-width: 768px) {
  header nav {
    ...    
    transition: clip-path var(--transition-long), 
      background-color var(--transition-long);

  }
}
```