DEV Community

Cover image for Create a responsive navbar with React and CSS
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Create a responsive navbar with React and CSS

Written by Chinwike Maduabuchi✏️

Styling responsive navigation menus for end users is hardly ever an easy process. Frontend developers have to consider certain parameters — like device breakpoints and accessibility — to create a pleasant navigation experience. It can get more challenging in frontend frameworks like React, where CSS-in-JS tends to get tricky.

In this post, you will learn how to create a responsive navbar with CSS and React Hooks. You can fiddle with the source code and view the live project here.

Navbar Demo

Prerequisites

To follow along with this tutorial, you'll need:

  • Basic understanding of React and React Hooks
  • Some CSS knowledge — particularly Flexbox

Now, let's set up a new React application!

Creating the project

To bootstrap a new React project in CodeSandbox, open a new browser tab and type in react.new. This will create a starter React application:

// App.js
import "./styles.css";
export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Currently, the default stylesheet for your app is located at the root, in styles.css. Let's edit this file to give our own feel to the page layout:

// styles.css
@import url("https://fonts.googleapis.com/css2?family=Karla:wght@200;400&display=swap");

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: "Karla", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
    "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.container {
  max-width: 90%;
  margin-right: auto;
  margin-left: auto;
  padding: 1rem;
}

article h1 {
  margin: 1rem 0;
}
Enter fullscreen mode Exit fullscreen mode

The rules above sets Karla as the primary font of the document and includes a container class for padding and aligning content on the page.

With that done, let's write some markup in App.js to display content:

// App.js
import Navbar from "./components/Navbar";
import "./styles.css";

function App() {
  return (
    <div>
      <Navbar />
      <div className="container">
        <article>
          <h1>What is Lorem Ipsum? </h1>
          Lorem Ipsum is simply dummy text of the printing and typesetting industry...
        </article>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Notice the Navbar component import on the second line. I've created the Navbar.js file through CodeSandbox’s sidebar in the following file path: src/components/Navbar.js.

The content of this file, so far, is the component’s default export statement, allowing you to import it into App.js:

// Navbar.js 
export default function Navbar() {
  return (
    <nav>Navigation</nav>
    )
}
Enter fullscreen mode Exit fullscreen mode

Understanding the navigation layout

Our goal is to create a responsive navbar that initially displays the nav menu — ul element — in a horizontal layout. On reaching a mobile viewport, the menu repositions under the navbar and spans the remaining height and width of the screen.

This layout will be achieved through a conflation of both Flexbox and CSS positioning rules.

Write the markup from the subsequent code block into Navbar.js:

// Navbar.js
export default function Navbar() {
  return (
    <nav className="navigation">
      <a href="/" className="brand-name">
        MacroSoft
      </a>
      <button className="hamburger">
        {/* icon from heroicons.com */}
        <svg
          xmlns="http://www.w3.org/2000/svg"
          className="h-5 w-5"
          viewBox="0 0 20 20"
          fill="white"
        >
          <path
            fillRule="evenodd"
            d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z"
            clipRule="evenodd"
          />
        </svg>
      </button>
      <div
        className="navigation-menu">
        <ul>
          <li>
            <a href="/home">Home</a>
          </li>
          <li>
            <a href="/about">About</a>
          </li>
          <li>
            <a href="/contact">Contact</a>
          </li>
        </ul>
      </div>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

The markup above includes the brand-name, hamburger icon, and the navigation-menu, which are the three elements of our navbar.

Now let's proceed to style this component.

Styling the navbar component

Create the stylesheet for the navbar component in the following file path: src/styles/navbar.css

And import it into Navbar.js:

// Navbar.js
import "../styles/navbar.css"
export default function Navbar() {
  return(
    {/* navbar markup */}
  )
}
Enter fullscreen mode Exit fullscreen mode

We’ll begin with the navigation class:

// navbar.css
.navigation {
  height: 60px;
  width: 100%;
  display: flex;
  align-items: center;
  position: relative;
  padding: 0.5rem 0rem;
  background-color: #fff;
  color: black;
  box-shadow: 0 2px 2px 2px rgba(9, 9, 9, 0.23);
}
Enter fullscreen mode Exit fullscreen mode

In here, we’ve set the navbar’s width to 100% so that it spans the full width of the device. By making this element a flex container and assigning a specific height property to it, Flexbox lets us use the align-items property to center the flex items vertically.

Also, setting position to relative on this element ensures that the position value of any child element is determined relative to it. You'll see this in effect momentarily.

Let's style brand-name and navigation-menu:

// navbar.css
.brand-name {
  text-decoration: none;
  color: black;
  font-size: 1.3rem;
  margin-left: 1rem;
}
.navigation-menu {
  margin-left: auto;
}
Enter fullscreen mode Exit fullscreen mode

The major rule in the code block above is margin-left: auto rule applied to navigation-menu. This pushes the menu to the far right by taking up the available space to the left of it.

Now we can style child elements of navigation-menu:

// navbar.css
.navigation-menu ul {
  display: flex;
  padding: 0;
}
.navigation-menu li {
  // removes default disc bullet for li tags and applies margin to left & right side
  list-style-type: none;
  margin: 0 1rem;
}
.navigation-menu li a {
  // increases the surface area of the anchor tag to span more than just the anchor text
  text-decoration: none;
  display: block;
  width: 100%;
}
Enter fullscreen mode Exit fullscreen mode

display: flex on the ul element turns it into a flex container. The child li elements are then arranged in a row, which is the default value of the flex-direction property. The other rules serve to make the navigation links look better.

Desktop Version Of Navbar

Let's go on to style the menu icon with the hamburger class:

// navbar.css
.hamburger {
  // removes default border on button element
  border: 0;
  height: 40px;
  width: 40px;
  padding: 0.5rem;
  border-radius: 50%;
  background-color: #283b8b;
  cursor: pointer;
  transition: background-color 0.2s ease-in-out;
// positions the icon to the right and center aligns it vertically
  position: absolute;
  top: 50%;
  right: 25px;
  transform: translateY(-50%);
  display: none;
}
.hamburger:hover {
  background-color: #2642af;
}
Enter fullscreen mode Exit fullscreen mode

Here, we’ve used CSS positioning to position the menu icon on the right side of the navbar.

Remember the parent nav element has position set to relative. Therefore, with the position property of the icon set to absolute, we can center the icon vertically along the borders of the parent element using the top and transform properties. Read more on CSS positioning if you’re curious how this works.

Since we want the menu icon to stay hidden until a mobile viewport is reached, let’s set its display property to none and proceed to style the mobile layout with CSS media queries.

Responsiveness with media queries

Media queries are a CSS feature that lets you specify how your content layout will respond to different conditions — such as a change in viewport width.

Queries are written using the @media rule, followed by the target media type and the breakpoint at which to apply the styles:

@media screen and (max-width: 768px) {
  // rules go here
}
Enter fullscreen mode Exit fullscreen mode

Here, max-width: 768px ensures the styles are implemented only when the device width is at 768px or lower.

Let's now display the hamburger icon:

// navbar.css
@media screen and (max-width: 768px) {
  .hamburger {
    display: block;
  }
}
Enter fullscreen mode Exit fullscreen mode

And hide the ul element in navigation-menu:

// navbar.css
@media screen and (max-width: 768px) {
  .navigation-menu ul {
    display: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

Your mobile layout should look like this so far:

Mobile Version Of Navbar

Generally, a navbar has two possible views: expanded and hidden. You can implement this into your application by having separate classes control these two navbar views.

We'll start by styling the expanded version of the menu. Subsequently, you will see how to toggle between these two views with Hooks:

// navbar.css
@media screen and (max-width: 768px) {
  .navigation-menu ul {
    // navigation menu is positioned to start 60px from the top of the document (which is directly below the navbar)
    position: absolute;
    top: 60px;
    left: 0;
    // stacks the li tags vertically 
    flex-direction: column;
   // makes menu span full height and width
    width: 100%;
    height: calc(100vh - 77px);
    background-color: white;
    border-top: 1px solid black;
  }
  .navigation-menu li {
    // centers link text and strips off margin
    text-align: center;
    margin: 0;
  }
  .navigation-menu li a {
    color: black;
    // increases the surface area of the anchor tag to span the full width of the menu
    width: 100%;
    padding: 1.5rem 0;
  }
  .navigation-menu li:hover {
    background-color: #eee;
  }
}
Enter fullscreen mode Exit fullscreen mode

Above, we’ve positioned the navbar 60px from the top of the document — directly below the navbar. To determine the height of this element, I’ve made use of the CSS calc function, and I’ll explain why.

Ideally, we want the menu to span the full height of the document by using the viewport height unit vh. But because viewport units take all elements on the page into consideration, the 60px navbar contributes to the overall height of the page, causing the navigation menu to take its extra viewport units from the bottom of the screen, thereby producing a vertical scrollbar.

The calc function helps us counter this by allowing us to perform calculations when specifying CSS property values. Therefore, subtracting 60px from 100vh produces the accurate height for the navigation menu.

The preceding styles should result in the layout below (the yellow area marks the end of the document):

Expanded Version Of Navbar

However, because we want another class to control the display of the navigation menu, we'll set its display to none:

// navbar.css
@media screen and (max-width: 768px) {
  .navigation-menu ul {
    /* previous styles */
    dipslay: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

And create an expanded class, attached to navigation-menu, that sets the display property back to block:

// navbar.css
@media screen and (max-width: 768px) {
  .navigation-menu.expanded ul {
    display: block;
  }
}
Enter fullscreen mode Exit fullscreen mode

At this stage, you can only observe the two states of the navbar by manually editing the markup in Navbar.js to include the expanded class.

Unless your user is a devtools expert, you wouldn't want to stop developing at this point. Let's use the menu icon to toggle between the different navbar views using React’s useState Hook.

Toggling the navbar view with useState

In order to monitor the current state of the navigation menu, we'll introduce state into the Navbar component.

Create an isNavExpanded state and give it an initial value of false as such:

// Navbar.js
import { useState } from "react;
import "../styles/navbar.css";
export default function Navbar(){
  const [isNavExpanded, setIsNavExpanded] = useState(false)

  return (
    <nav className="navigation">
      <a href="/" className="brand-name">
        MacroSoft
      </a>
      <button className="hamburger" >
        {/* hamburger svg code... */}
      </button>
      {/* nav menu code... */}
    </nav>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now let's use the onClick event handler on the button element to toggle the isNavExpanded state:

// Navbar.js
import { useState } from "react;
import "../styles/navbar.css";
export default function Navbar(){
  const [isNavExpanded, setIsNavExpanded] = useState(false)

  return (
    <nav className="navigation">
      <a href="/" className="brand-name">
        MacroSoft
      </a>
      <button
        className="hamburger"
        onClick={() => {
          setIsNavExpanded(!isNavExpanded);
        }}
      >
        {/* hamburger svg code... */}
      </button>
      {/* nav menu code... */}
    </nav>
  )
}
Enter fullscreen mode Exit fullscreen mode

Here, we’ve called an anonymous arrow function within the onClick event handler. This function uses the updater function setIsNavExpanded to reverse the current value of the isNavExpanded state.

This means isNavExpanded will toggle between true and false whenever the icon is clicked.

You can now use the JavaScript ternary operator to conditionally pick the appropriate class name for the navigation menu based off isNavExpanded's value:

// Navbar.js
import { useState } from "react"
import "../styles/navbar.css"

export default function Navbar() {
  const [isNavExpanded, setIsNavExpanded] = useState(false)

  return (
    <nav className="navigation">
      <a href="/" className="brand-name">
        MacroSoft
      </a>
      <button
        className="hamburger"
        onClick={() => {
          setIsNavExpanded(!isNavExpanded)
        }}
      >
        {/* hamburger svg code... */}
      </button>
      <div
        className={
          isNavExpanded ? "navigation-menu expanded" : "navigation-menu"
        }
      >
        <ul>
          <li>
            <a href="/home">Home</a>
          </li>
          <li>
            <a href="/about">About</a>
          </li>
          <li>
            <a href="/contact">Contact</a>
          </li>
        </ul>
      </div>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

And that's a wrap! Now you should have a fully functional, responsive navbar.

Final Navbar

Conclusion

Navigation menus serve an important role in the overall experience of your web application. It's usually the first component your user comes in contact with when trying to figure out your app. Therefore, it'll serve you well to make it as organized and accessible as possible because it boosts user experience and even SEO performance.


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.

Latest comments (0)