Step-by-Step Guide: How to Add a Responsive Hamburger Menu in Next.js Using CSS
Creating a responsive hamburger menu is a must for modern web applications, especially when designing for mobile devices. In this tutorial, we’ll walk you through how to build a responsive hamburger menu in Next.js using CSS. Whether you’re a beginner or an experienced developer, this guide will help you implement a clean and functional navigation menu that adapts seamlessly to different screen sizes. By the end of this post, you’ll have a fully working menu ready to enhance your Next.js projects.
Let’s get started and make your navigation user-friendly and responsive!
Before we start, we shall add custom properties, utility classes and reset code into our css. For those, take a look at these blogs -> https://medium.com/@greemjellyfish/css-reset-before-the-start-of-a-project-b1e26e59dbd3, https://medium.com/@greemjellyfish/css-utility-classes-sr-color-typography-9abbf3e01b86
- First, we will create a header and a button, then hide the ‘menu’ span, making it accessible only to screen readers.
//Nav.tsx
import React from 'react'
export default function Nav() {
return (
<header>
<button>
<span className = "sr-only">
Menu
</span>
</button>
</header>
)
}css
/* screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
white-space: nowrap; /* added line */
border: 0;
}2. Next, we add the class, className= "mobile-nav-toggle" to use it as a burger icon. We will write the CSS for it later.
<button className="mobile-nav-toggle">
<span className = "sr-only">
Menu
</span>
</button>3. add aria-controls=”primary-navigation” for improving accessibility for assistive technologies
<button className="mobile-nav-toggle"
aria-controls="primary-navigation">
<span className = "sr-only">
Menu
</span>
</button>4. match <button>’s aria-controls value, primary-navigation , with<ul>’s id , primary-navigation link them together by matching the value of those attributes. The button is intended to show or hide the navigation. So when the menu is activated, the entire navigation will open or close.
aria-controls attribute only works with id, not class so we are using id .
<header>
<button className="mobile-nav-toggle"
aria-controls="primary-navigation"> //<--------------
<span className = "sr-only">
Menu
</span>
</button>
<nav>
<ul id="primary-navigation"> //<--------------
<li>Home</li> <li>Portfolio</li> <li>About</li>
</ul>
</nav>
</header>5. add aria-expanded attribute. The menu is closed when it’s false, when it’s clicked, it will open. We will used NextJS’s javascript to change the value to true or false .
<header>
<button className="mobile-nav-toggle"
aria-controls="primary-navigation">
<span className = "sr-only"
aria-expanded="false">
Menu
</span>
</button>
<nav>
<ul id="primary-navigation">
<li>Home</li> <li>Portfolio</li> <li>About</li>
</ul>
</nav>
</header>6. add NextJS’s links for the each navigation links
import Link from '@/node_modules/next/link'
import React from 'react'
export default function Nav() {
return (
<header>
<button className="mobile-nav-toggle"
aria-controls="primary-navigation">
<span className = "sr-only"
aria-expanded="false">
Menu
</span>
</button>
<nav>
<ul id="primary-navigation"
class="primary-navigation>
<li>
<Link href="/projects">
Projects
</Link>
</li>
<li>
<Link href="/services">
Services
</Link>
</li>
<li>
<Link href="/about">
About
</Link>
</li>
<li>
<Link href="/blog">
Blog
</Link>
</li>
</ul>
</nav>
</header>
)
}7. Style the navigation
/* css */
.primary-navigation {
list-style-type: none;
padding: 0 0 0 1.5em;
margin: 0;
}8. add flex layout
// Nav.tsx
<nav>
<ul id="primary-navigation"
className="flex
primary-navigation">
<li>
<Link href="/projects">
Projects
</Link>
//css
/* general utility class */
.flex {
display: flex;
}9. center the menu
.primary-navigation {
list-style-type: none;
padding: 2rem 0 0 0em;
margin: 0 auto;
flex-direction: column;
align-items: center;
text-align: center;
}
.primary-navigation a {
text-decoration: none;
}10. Adding Media Queries for hamburger menu:
For the hamburger menu, we are using max-width instead of min-width (mobile-first approach) to avoid overwriting the intricately crafted navigation code.
/* ------------------------------------------------------------------------------------ */
/* -----------------------------------------n-a-v-------------------------------------- */
/* ------------------------------------------------------------------------------------ */
.primary-navigation {
list-style-type: none;
margin: 0 auto;
padding: 0 0.5rem 0 0;
justify-content: center;
gap: 5rem;
z-index: 10000;
}
.primary-navigation li a{
font-weight: 500;
}
@media(max-width: 35rem){
.primary-navigation {
padding: 2rem 0 0 0em;
flex-direction: column;
align-items: center;
text-align: center;
gap: .5rem;
}
}
.primary-navigation a {
text-decoration: none;
}🍔
- Let’s add the hamburger icon
export default function Nav() {
return (
<header>
<button className="mobile-nav-toggle" ///<-----------------
aria-controls="primary-navigation">
<span className = "sr-only"
aria-expanded="false">
Menu
</span>
</button>
<nav>
.
.
.
.css
/* global scope */
.mobile-nav-toggle {
display: none;
}
@media(max-width: 45rem){
.mobile-nav-toggle {
border: 0;
background-color: hsl( var(--clr-white) );
display: block;
position: absolute;
z-index:1000;
right: 1em;
top: 1.3em;
background-image: url(/burger.svg);
background-size: 100%;
width: 2em;
background-repeat: no-repeat;
aspect-ratio: 1/1;
}
.primary-navigation {
background-color: var(--clr-white);
border-top: 1px black solid;
border-bottom: 1px black solid;
margin-top: 4rem;
padding: 2rem 0 2rem 0rem;
flex-direction: column;
align-items: center;
text-align: center;
gap: 2rem;
}
}yay we get the burger button!
2. new problem: need to make the hamburger button a toggle switch.
solution: let’s solve it with NextJS to close the menu
import React, { useState, useEffect} from 'react'
export default function Nav() {
const [burgerOn, setBurgerOn ] = useState(false) //<------------------ 2
const toggleBurger = () => { //<------------------ 3
setBurgerOn((prev) => !prev)
console.log(burgerOn)
}
return (
<header>
<button className={`mobile-nav-toggle //<------------------ 4
${burgerOn? 'burger-off': 'burger-on'}
`}
aria-controls="primary-navigation"
onClick={toggleBurger}> //<------------------ 1css
@media(max-width: 45rem){
...
.burger-on {
background-image: url(/burger.svg);
}
.burger-off {
background-image: url(/burger-x.svg);
}
}3. using the same logic, we will update our aria-expanded attribute as well.
<button className={`mobile-nav-toggle
${burgerOn? 'burger-off': 'burger-on'}
`}
aria-controls="primary-navigation"
onClick={toggleBurger}>
<span className = "sr-only"
aria-expanded=
{burgerOn? 'false': 'true'
}>
Menu
</span>
</button>4. problem: navigation still shows even if the toggle switch has been activated.
<nav>
<ul id="primary-navigation"
className={`flex
primary-navigation
${burgerOn? 'show-nav': 'hide-nav'}
`}>
<li>
......
....css
@media(max-width: 45rem){
.primary-navigation {
...
}
.hide-nav{
display: none;
}6. Let’s do some clean up for the styling.
"use client"
import Image from "next/image"
import Link from '@/node_modules/next/link'
import React, { useState, useEffect} from 'react'
export default function Nav() {
const [burgerOn, setBurgerOn ] = useState(false)
const toggleBurger = () => {
setBurgerOn((prev) => !prev)
}
return (
<header >
<div className="logo-toggle-container">
<Link href="/" replace >
<Image
src="/logo-letter-lg.svg"
alt="orange time & space logo"
className="logo-letter-lg"
width="150"
height="50"/>
<Image
src="/logo-letter-sm.svg"
alt="orange time & space logo"
className="logo-letter-sm"
width="50"
height="50"/>
</Link>
<button className={`mobile-nav-toggle
${burgerOn? 'burger-off': 'burger-on'}
`}
aria-controls="primary-navigation"
onClick={toggleBurger}>
<span className = "sr-only"
aria-expanded=
{burgerOn? 'false': 'true'
}>
Menu
</span>
</button>
</div>
<nav>
<ul id="primary-navigation"
className={`flex
primary-navigation
${burgerOn? 'show-nav': 'hide-nav'}
`}>
<li>
<Link href="/projects" className="fs-300">
Projects
</Link>
</li>
<li>
<Link href="/services" className="fs-300">
Services
</Link>
</li>
<li>
<Link href="/about" className="fs-300">
About
</Link>
</li>
<li>
<Link href="/blog" className="fs-300">
Blog
</Link>
</li>
<li>
<Link href="/contact" className="fs-300 nav-contact">
Contact
</Link>
</li>
</ul>
</nav>
</header>
)
}css
/* ------------------------------------------------------------------------------------ */
/* -----------------------------------------n-a-v-------------------------------------- */
/* ------------------------------------------------------------------------------------ */
.logo-toggle-container {
padding-inline: 1rem;
margin-inline: auto;
max-width: 100rem;
margin: 1rem 0 1rem 0;
}
.primary-navigation {
position:relative;
list-style-type: none;
margin: auto;
justify-content: center;
top: -3rem;
gap: 5rem;
z-index: 10000;
}
.primary-navigation > li:nth-last-child(-n+2) {
margin-right: 0;
}
.primary-navigation li a{
font-weight: 500;
}
.primary-navigation a {
text-decoration: none;
color:hsl( var(--clr-black) )
}
.mobile-nav-toggle {
display: none;
}
.logo-letter-lg {
display: block;
}
.logo-letter-sm {
display: none;
}
.nav-contact {
position: absolute;
top: -0.8rem;
right: 1rem;
padding: 1rem 2rem 1rem 2rem;
border: 1px solid hsl( var(--clr-black-200) );
border-radius: 15px;
}
.nav-contact:hover {
background-color: hsl( var(--clr-orange));
}
@media(max-width: 45rem){
.primary-navigation {
position:static;
background-color: var(--clr-white);
border-top: 1px black solid;
border-bottom: 1px black solid;
padding: 2rem 0 3rem 0rem;
flex-direction: column;
text-align: center;
gap: 3rem;
}
.hide-nav{
display: none;
}
.mobile-nav-toggle {
border: 0;
background-color: hsl( var(--clr-white) );
display: block;
position: absolute;
z-index:1000;
right: 1em;
background-size: 100%;
width: 2em;
background-repeat: no-repeat;
aspect-ratio: 1/1;
}
.burger-on {
background-image: url(/burger.svg);
top: 2rem;
cursor: pointer;
}
.burger-off {
background-image: url(/burger-x.svg);
top: 1.5rem;
cursor: pointer;
}
.logo-container {
display: grid;
align-items: center;
}
.logo-letter-lg {
display: none;
}
.logo-letter-sm {
display: block;
}
.nav-contact {
margin-top: 50px;
position:static;
}
}Lastly how to structure routes
Read more about how to use app/about/page.tsx to create the /about route. -> https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating
