HTML and CSS Only Hamburger Menu

For web developers looking to implement a hamburger menu without having to utilize JavaScript

Introduction

The HTML and CSS only hamburger menu is exactly as it sounds, a simple and effective way to create a responsive menu system for a website without using any JavaScript. Whether it is a lack of JavaScript knowledge, or the desire to utilize as little JS as possible, this documentation will lay out in detail exactly how to do so.

The basis for this hamburger menu system was adapted from Erik Terwan's "Pure CSS Hamburger fold-out menu" found on CodePen. Links and more information can be found towards the bottom of the page in the references and acknowledgements section.


Prerequisites

In order to get the most out of this documentation, knowledge of the following technologies and software are recommended:

Further information on HTML, CSS, code editors and developer tools can be found in the links above.


Structure (HTML)

The structure of the menu system is broken up into two parts. The first is a #nav element that encapsulates everything needed to create:

The second part of the structure is what creates the sidebar navigation menu thats active when being viewed on screens wider than 823 pixels.

Wrapping the entirety of the hamburger menu is a nav element with the id of #navbar. This contains the #menu-toggle element.

<nav id="navbar">
  <div id="menu-toggle">
  </div>
</nav>

Nested inside of "menu-toggle" is a checkbox input to toggle the slide-out menu.

<nav id="navbar">
  <div id="menu-toggle">
    <input type="checkbox" />
  </div>
</nav>

Directly underneath the checkbox toggle are three span elements that will be styled to create the infamous hamburger menu icon.

<nav id="navbar">
  <div id="menu-toggle">
    <input type="checkbox" />
    <span></span>
    <span></span>
    <span></span>
  </div>
</nav>

After the three span elements, a sibling div with an id of #slide-out-menu is used to create the menu pane that is displayed when the checkbox input is toggled. Inside this div element is where you place whatever internal page links you need for your site as list items, wrapped in an unordered list element. In this example, Home, About, Info, and Contact are used for illustration purposes.

<nav id="navbar">
  <div id="menu-toggle">
    <input type="checkbox" />
    <span></span>
    <span></span>
    <span></span>
    <div id="slide-out-menu">
      <ul>
        <li><a class="nav-link" href="#Home">Home</a></li>
        <li><a class="nav-link" href="#About">About</a></li>
        <li><a class="nav-link" href="#Info">Info</a></li>
        <li><a class="nav-link" href="#Contact">Contact</a></li>
      </ul>
    </div>
  </div>
</nav>

There we have the entirety of the hamburger menu icon, the checkbox toggle, and the slide-out menu.

Moving on to the second half of the menu system, we have a div with the id of #sidebar. This sits directly below the nav element, as its sibling.

<div id="sidebar">
</div>

This is used as a container for the internal page links.

<div id="sidebar">
  <ul>
    <li><a class="nav-link" href="#Home">Home</a></li>
    <li><a class="nav-link" href="#About">About</a></li>
    <li><a class="nav-link" href="#Info">Info</a></li>
    <li><a class="nav-link" href="#Contact">Contact</a></li>
  </ul>
</div>

And with that, we have the complete structure for the HTML and CSS only hamburger menu. Putting it all together, it looks like this:

<nav id="navbar">
  <div id="menu-toggle">
    <input type="checkbox" />
    <span></span>
    <span></span>
    <span></span>
    <div id="slide-out-menu">
      <ul>
        <li><a class="nav-link" href="#Home">Home</a></li>
        <li><a class="nav-link" href="#About">About</a></li>
        <li><a class="nav-link" href="#Info">Info</a></li>
        <li><a class="nav-link" href="#Contact">Contact</a></li>
      </ul>
    </div>
  </div>
</nav>
<div id="sidebar">
  <ul>
    <li><a class="nav-link" href="#Home">Home</a></li>
    <li><a class="nav-link" href="#About">About</a></li>
    <li><a class="nav-link" href="#Info">Info</a></li>
    <li><a class="nav-link" href="#Contact">Contact</a></li>
  </ul>
</div>

*Disclaimer* the topic of whether to use unordered lists to mark up navigation links on websites has been debated for some time now. Personally, with the information I have right now, I choose to utilize these elements when making my site's navigation. Articles have been written debating the pros and cons of each. CSS-Tricks wrote two different articles on the topic, you can find both of them here, and here.


Styling (CSS)

When it comes to styling, each application is going to have different needs and design parameters to follow. This section will serve to show you the basics on how to style the menus just as you see here.

The CSS is organized in the same fashion the HTML is, in three separate sections;

Hamburger Menu Icon

At the top, the first thing that is styled is the hamburger menu icon. That is comprised of;

Within the #menu-toggle element, there is also the #slide-out-menu element, but that will be styled separately in the next section.

The first selector is used to set the display and position of the entire #menu-toggle element.

#menu-toggle {
  display: inline-block;
  position: fixed;
  top: 36px;
  left: 15px;
  z-index: 1;
}       
#menu-toggle
Property & Value Explanation
display:inline-block;
Allows the div to flow with text as an inline element, but allows for block-level properties such as width and height.
position:fixed;
positions the element relative to the browser window and makes it stay in the same place even if the user scrolls the page
top: 36px;
Positions the element 36 pixels from the top of the browser window
left: 15px;
Positions the element 15 pixels from the left of the browser window
z-index: 1;
Determines the stacking order of elements. An element with a higher z-index will be displayed in front of elements with lower value. This makes sure the 'menu-toggle' elements displays on top of everything else on the page besides the checkbox input

The next selector targets the input element - in this case the checkbox selector - inside the #menu-toggle element

#menu-toggle input {
  display: block;
  width: 40px;
  height: 31px;
  position: absolute;
  top: -7px;
  left: -9px;
  cursor: pointer;
  opacity: 0;
  z-index: 2;
}       
#menu-toggle input
Property & Value Explanation
display:block;
Causes the element to generate a block-level box
width: 40px;
Sets the with of the 'input' element to 40 pixels
height: 31px;
Sets the height of the 'input' element to 31 pixels
position: absolute;
Positions the element relative to its closest positioned ancestor. In this case the #menu-toggle element
top: -7px;
Moves the element 7 pixels upwards of its original position
left: -9px;
Moves the element 9 pixels to the left of its original position
cursor: pointer;
Sets the cursor to a pointer hand icon when the mouse is over the 'input' element, indicating that the element is clickable.
opacity: 0;
Makes the element transparent and invisible
z-index: 2;
Stacks the 'input' element on top of the #menu-toggle element, including the empty spans

The following selector targets the three empty span elements, and turns them into the three layers of the hamburger menu icon that we see on the page. It also sets up the transition effect used on individual spans in the following selectors to create the 'X' once the input toggle is activated.

#menu-toggle span {
  display: block;
  width: 33px;
  height: 4px;
  margin-bottom: 5px;
  position: relative;
  background: #cdcdcd;
  border-radius: 3px;
  z-index: 1;
  transition: transform 500ms cubic-bezier(0.77, 0.2, 0.05, 1),
    background 500ms cubic-bezier(0.77, 0.2, 0.05, 1), opacity 550ms ease;
  }     
#menu-toggle span
Property & Value Explanation
display: block;
Makes all the span elements block level elements to display separately on different lines, with a line break between them
width: 33px;
Sets the width of all span elements to 33 pixels
height: 4px;
Sets the height of all span elements to 4 pixels
margin-bottom: 5px;
Adds a 5 pixel margin to the bottom of each span to give some visual separation
position: relative;
Positions the span elements relative to the #menu-toggle parent element
background: #cdcdcd;
Set the span elements to a light grey color
border-radius: 3px;
Rounds the ends of each span element
z-index: 1;
Places the span elements underneath the (now transparent) input checkbox toggle
transition: transform 500ms cubic-bezier(0.77, 0.2, 0.05, 1),
  background 500ms cubic-bezier(0.77, 0.2, 0.05, 1), opacity 550ms ease;
Sets up the transition effect for the subsequent 'transform', 'background', and 'opacity' properties in later individual span CSS selectors, with specific timing functions and durations for each.

The next five selectors will be looked at as a group. The first two - #menu-toggle span:nth-child(2) and #menu-toggle span:nth-child(4) - targets the top and bottom span elements, and sets the transform origin for each.

/* nth-child(2) == top (1st) span */
#menu-toggle span:nth-child(2) {
  transform-origin: top left;
}

/* nth-child(4) == last (3rd) span */
#menu-toggle span:nth-child(4) {
  transform-origin: bottom left;
}       

Since the 'top' span is technically the second sibling ancestor to the #menu-toggle parent element - the checkbox input element is the first child in this case - it has to be targeted as 'span:nth-child(2)'.

The other three selectors call upon the transition property that was set in the #menu-toggle span selector earlier with timing and durations for the following transform, opacity, and background properties. This is only applied when the input element is in the active, or 'checked', state.

#menu-toggle input:checked ~ span:nth-child(2) {
  transform: rotate(45deg) translate(-3px);
  background: var(--span-X-color);
}

#menu-toggle input:checked ~ span:nth-child(3) {
  opacity: 0;
  transform: scale(0, 0);
}

#menu-toggle input:checked ~ span:nth-child(4) {
  transform: rotate(-45deg) translate(-3px);
  background: var(--span-X-color);
}       
#menu-toggle input:checked ~ span:nth-child(2)
Property & Value Explanation
transform: rotate(45deg) translate(-3px);
Rotates the first (top) span 45 degrees clockwise and moves it 3 pixels to the left. This 'translate' is to make sure the top and bottom spans intersect directly in the middle and form a 90 degree angle together
background: var(--span-X-color);
Transitions the color of the span to a deep, dark grey. In this case set to a variable to make changing the color of both the top and bottom span elements easier.
#menu-toggle input:checked ~ span:nth-child(3)
Property & Value Explanation
opacity: 0;
Makes the middle span element invisible
transform: scale(0, 0);
Shrinks the middle span down to zero
#menu-toggle input:checked ~ span:nth-child(4)
Property & Value Explanation
transform: rotate(-45deg) translate(-3px);
Rotates the last (bottom) span 45 degrees counter-clockwise and does it 3 pixels to the left. This 'translate' is to make sure the top and bottom spans intersect directly in the middle and form a 90 degree angle together
background: var(--span-X-color);
Transitions the color of the span to a deep, dark grey. In this case, it is set to a variable to make changing the color of both the top and bottom span elements easier.

With that last selector complete, we have everything needed to position, style, and animate the hamburger menu icon and checkbox input toggle. The next section will break down the selectors used to style the slide-out menu that appears when the hamburger menu icon is activated.

Slide-out Menu

The first selector in this section targets the #slide-out-menu element and sets it's positioning, size, text, spacing, color, and animations.

#slide-out-menu {
  position: fixed;
  width: min-content;
  top: 0;
  left: 0;
  padding: 30px 24px 0px 58px;
  line-height: 1.25em;
  background: var(--menu-bg-color);
  font-family: var(--menu-font);
  transform-origin: top left;
  transform: translate(-100%, 0);
  transition: transform 500ms cubic-bezier(0.77, 0.2, 0.05, 1);
}       
#slide-out-menu
Property & Value Explanation
position: fixed;
Sets the element to a fixed position so it remains in the same place in the viewport as the user scrolls.
width: min-content;
Sets the elements width to the smallest possible required to fit it's content.
top: 0;
Positions the element at the top edge of the viewport.
left: 0;
Positions the element at the left edge of the viewport.
padding: 107px 24px 0px 58px;
Sets the element's padding to 107px (top), 24px (right), 0px (bottom), and 58px (left).
line-height: 1.25em;
Sets the element's line height to 1.25 times its font size.
background: var(--menu-bg-color);
Sets the background color to light grey, in this case set to a variable to make changing the background of both the slide-out menu and the sidebar easier and more consistent.
font-family: var(--menu-font);
Sets the font of the menu link text to "Abel" with a sans-serif fallback font, set to a variable to make changing the font for both the slide-out menu and the sidebar easier and more consistent.
transform-origin: top left;
Sets the origin point for the element's CSS transformations to the top left corner.
transform: translate(-100%, 0);
Moves the element to the left by 100% of its width.
transition: transform 500ms cubic-bezier(0.77, 0.2, 0.05, 1);
Specifies an effect for the 'transform' property set above, making it transition over 500 milliseconds using a cubic-bezier timing function.

With the styling of the slide-out menu container itself taken care of, the next five selectors target the contents of that container.

The first three selectors in this group handle the spacing, sizing, color, and styling of the list items and menu links. The '#slide-out-menu .nav-link' selector also sets up a transition value used to fade in the text color when hovering over the links.

#slide-out-menu li {
  padding-bottom: 20px;
}

#slide-out-menu .nav-link {
  font-size: 1.5em;
  text-decoration: none;
  color: var(--menu-item-color);
  transition: color 300ms ease;
}

#slide-out-menu a:hover {
  color: var(--menu-link-hover-color);
}
#slide-out-menu li
Property & Value Explanation
padding-bottom: 20px;
Adds 20 pixels of padding to the bottom of every menu list item in the slide-out menu.
#slide-out-menu .nav-link
Property & Value Explanation
font-size: 1.5em;
Sets the font size to 1.5 times the default font size of the element.
text-decoration: none;
Removes the underline from the text decoration of the links.
color: var(--menu-item-color);
Sets the menu link color to a black color, in this case set to a variable to make changing the color of all of the items contained in the menus easier and more consistent.
transition: color 300ms ease;
Specifies a smooth transition effect for the color change, lasting 300ms and using an ease timing function.
#slide-out-menu a:hover
Property & Value Explanation
color: var(--menu-link-hover-color);
Sets the color for the link hover state to a turquoise color, in this case set to a variable to make changing the color of the link hover states in both the slide-out menu and the sidebar menu easier and more consistent.

The last two selectors for the slide-out menu hide the hamburger menu icon when the sidebar menu is visible on wider screens, and slide the menu in from the left when the input is in the checked state.

nav {
  display: none;
  }
  
#menu-toggle input:checked ~ #slide-out-menu {
  transform: none;
}
nav
Property & Value Explanation
display: none;
Hides the hamburger menu icon when the sidebar menu is visible on wider screens.
#menu-toggle input:checked ~ #slide-out-menu
Property & Value Explanation
transform: none;
Slides the menu in from the left of the screen, reversing the transform: translate(-100%, 0); set up in the #slide-out-menu selector.

That completes the CSS styling for the hamburger menu icon and slide-out menu. In the following section, we will take a look at everything needed to style the sidebar menu visible when being viewed on wider screens.

Sidebar

The first selector - in a series of four total, used to style the sidebar menu and contents - targets the #sidebar element itself and sets all the standard parameters for itself and the contents such as size, positioning, spacing, coloring and font.

#sidebar {
  height: 100dvh;
  width: var(--side-bar-width);
  position: fixed;
  top: 0px;
  padding: 10px;
  background-color: var(--menu-bg-color);;
  line-height: 1em;
  font-family: var(--menu-font);
  font-size: 1.2em;
  box-shadow: 4px 0px 4px rgba(0, 0, 0, 0.25);
}
#sidebar
Property & Value Explanation
height: 100dvh;
Sets the height of the #sidebar element to 100 dynamic viewport height units (dvh). This ensures that the sidebar's height spans the entire vertical height of the viewport whether the user agent interface is currently visible or not.
width: var(--side-bar-width);
Sets the sidebar width to 153 pixels (assigned to a variable) making it easier to adjust both the sidebar width and left side footer padding in one place
position: fixed;
Fixes the element relative to the viewport, even when the page is scrolled.
top: 0px;
Positions the top edge of the element at 0 pixels from the top of the viewport.
padding: 10px;
Adds 10 pixels of padding to the content inside the element, creating space between the content and the edges of the sidebar.
background-color: var(--menu-bg-color);
Sets the background color to light grey, in this case set to a variable to make changing the background of both the sidebar and the slide-out menu easier and more consistent.
line-height: 1em;
Sets the line height of the text inside the element to the height of a single line of text.
font-family: var(--menu-font);
Sets the font of the menu link text to "Abel" with a sans-serif fallback font, set to a variable to make changing the font for both the sidebar and the slide-out menu easier and more consistent.
font-size: 1.2em;
Sets the font size of the text inside the element to 1.2 times the default font size.
box-shadow: 4px 0px 4px rgba(0, 0, 0, 0.25);
Adds a box shadow to the right side of the element.

With the sidebar element established, the last three CSS selectors focus on styling the contents of the sidebar such as the list items, the navigation links, and the link hover states.

#sidebar li {
  padding-bottom: 20px;
}

#sidebar .nav-link {
  display: block;
  text-decoration: none;
  color: var(--menu-item-color);
  transition: color 300ms ease;
}

#sidebar a:hover {
  color: var(--menu-link-hover-color);
}
#sidebar li
Property & Value Explanation
padding-bottom: 20px;
Adds 20 pixels of padding to the bottom of each list item in the sidebar element.
#sidebar .nav-link
Property & Value Explanation
display: block;
Sets the display property of the .nav-link elements to block. This changes the display behavior of the links from inline (default for anchor elements) to block-level, which means each link will take up the full width of its container and start on a new line.
text-decoration: none;
Removes the underline from the text decoration of the .nav-link elements.
color: var(--menu-item-color);
Sets the menu link color to a black color, in this case set to a variable to make changing the color of all of the items contained in the menus easier and more consistent.
transition: color 300ms ease;
Specifies a smooth transition effect for the color change, lasting 300ms and using an ease timing function.
#sidebar a:hover
Property & Value Explanation
color: var(--menu-link-hover-color);
Sets the color for the link hover state to a turquoise color, in this case set to a variable to make changing the color of the link hover states in both the sidebar menu and the slide-out menu easier and more consistent.

With that, we have everything needed to style our hamburger icon, both of our menus, and all of their contents just as you see on the page here. Just like we did in the HTML section, when we put it all together, it looks like this:

/* ---------▽-Hamburger Menu Icon-▽--------- */

#menu-toggle {
  display: inline-block;
  position: fixed;
  top: 36px;
  left: 15px;
  z-index: 1;
}

#menu-toggle input {
  display: block;
  width: 40px;
  height: 31px;
  position: absolute;
  top: -7px;
  left: -9px;
  cursor: pointer;
  opacity: 0;
  z-index: 2;
}

#menu-toggle span {
  display: block;
  width: 33px;
  height: 4px;
  margin-bottom: 5px;
  position: relative;
  background: var(--menu-bg-color);
  border-radius: 3px;
  z-index: 1;
  transition: transform 500ms cubic-bezier(0.77, 0.2, 0.05, 1),
    background 500ms cubic-bezier(0.77, 0.2, 0.05, 1), opacity 550ms ease;
}

#menu-toggle span:nth-child(2) {
  transform-origin: top left;
}

#menu-toggle span:nth-child(4) {
  transform-origin: bottom left;
}

#menu-toggle input:checked ~ span:nth-child(2) {
  transform: rotate(45deg) translate(-3px);
  background: var(--menu-item-color);
}

#menu-toggle input:checked ~ span:nth-child(3) {
  opacity: 0;
  transform: scale(0, 0);
}

#menu-toggle input:checked ~ span:nth-child(4) {
  transform: rotate(-45deg) translate(-3px);
  background: var(--menu-item-color);
}

/* ---------△-Hamburger Menu Icon-△--------- */
/* ---------▽-Slide-Out-Menu-▽--------- */

#slide-out-menu {
  position: fixed;
  width: min-content;
  top: 0;
  left: 0;
  padding: 30px 24px 0px 58px;
  line-height: 1.25em;
  background: var(--menu-bg-color);
  font-family: var(--menu-font);
  transform-origin: top left;
  transform: translate(-100%, 0);
  transition: transform 500ms cubic-bezier(0.77, 0.2, 0.05, 1);
}

#slide-out-menu li {
  padding-bottom: 20px;
}

#slide-out-menu .nav-link {
  font-size: 1.5em;
  text-decoration: none;
  color: var(--menu-item-color);
  transition: color 300ms ease;
}

#slide-out-menu a:hover {
  color: var(--menu-link-hover-color);
}

nav {
  display: none;
}

#menu-toggle input:checked ~ #slide-out-menu {
  transform: none;
}

/* ---------△-Slide-Out-Menu-△--------- */
/* ---------------▽-Sidebar-▽--------------- */

#sidebar {
  height: 100dvh;
  width: var(--side-bar-width);
  position: fixed;
  top: 0px;
  padding: 10px;
  background-color: var(--menu-bg-color);
  line-height: 1em;
  font-family: var(--menu-font);
  font-size: 1.2em;
  box-shadow: 4px 0px 4px rgba(0, 0, 0, 0.25);
}

#sidebar li {
  padding-bottom: 20px;
}

#sidebar .nav-link {
  display: block;
  text-decoration: none;
  color: var(--menu-item-color);
  transition: color 300ms ease;
}

#sidebar a:hover {
  color: var(--menu-link-hover-color);
}

References and Acknowledgments

This technical documentation page was designed and created in part for the freeCodeCamp responsive web design curriculum. This is the third of five projects required for the certification. This site is a reworking and an expansion of my original design submitted for credit towards the certification.

While the design and implementation of this specific site is completely my own, the original idea for the HTML and CSS Only Hamburger Menu was created by Erik Terwan. I've since modified it to suit my aesthetic and this project specifically. You can find the code for the original implementation on CodePen.