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:
- HTML: HyperText Markup Language
- CSS: Cascading Style Sheets
- A code editor such as VS Code or VIM
- Browser base developer tools in Chrome, Firefox or similar
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 hamburger icon
- The checkbox input used as a click receiver to toggle the slide-out menu
- The slide-out menu
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;
- The hamburger menu icon, comprised of the checkbox input selector and the three spans making up the icon itself
- The slide-out menu and containing page links
- The side bar menu visible on larger screens
Hamburger Menu Icon
At the top, the first thing that is styled is the hamburger menu icon. That is comprised of;
- The #menu-toggle element
- Checkbox input toggle
- Three empty span elements
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 |
|
Allows the div to flow with text as an inline element, but allows for block-level properties such as width and height. |
|
positions the element relative to the browser window and makes it stay in the same place even if the user scrolls the page |
|
Positions the element 36 pixels from the top of the browser window |
|
Positions the element 15 pixels from the left of the browser window |
|
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 |
|
Causes the element to generate a block-level box |
|
Sets the with of the 'input' element to 40 pixels |
|
Sets the height of the 'input' element to 31 pixels |
|
Positions the element relative to its closest positioned ancestor. In this case the #menu-toggle element |
|
Moves the element 7 pixels upwards of its original position |
|
Moves the element 9 pixels to the left of its original position |
|
Sets the cursor to a pointer hand icon when the mouse is over the 'input' element, indicating that the element is clickable. |
|
Makes the element transparent and invisible |
|
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 |
|
Makes all the span elements block level elements to display separately on different lines, with a line break between them |
|
Sets the width of all span elements to 33 pixels |
|
Sets the height of all span elements to 4 pixels |
|
Adds a 5 pixel margin to the bottom of each span to give some visual separation |
|
Positions the span elements relative to the #menu-toggle parent element |
|
Set the span elements to a light grey color |
|
Rounds the ends of each span element |
|
Places the span elements underneath the (now transparent) input checkbox toggle |
|
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 |
|
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 |
|
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 |
|
Makes the middle span element invisible |
|
Shrinks the middle span down to zero |
#menu-toggle input:checked ~ span:nth-child(4) | |
---|---|
Property & Value | Explanation |
|
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 |
|
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 |
|
Sets the element to a fixed position so it remains in the same place in the viewport as the user scrolls. |
|
Sets the elements width to the smallest possible required to fit it's content. |
|
Positions the element at the top edge of the viewport. |
|
Positions the element at the left edge of the viewport. |
|
Sets the element's padding to 107px (top), 24px (right), 0px (bottom), and 58px (left). |
|
Sets the element's line height to 1.25 times its font size. |
|
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. |
|
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. |
|
Sets the origin point for the element's CSS transformations to the top left corner. |
|
Moves the element to the left by 100% of its width. |
|
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 |
|
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 |
|
Sets the font size to 1.5 times the default font size of the element. |
|
Removes the underline from the text decoration of the links. |
|
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. |
|
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 |
|
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 |
|
Hides the hamburger menu icon when the sidebar menu is visible on wider screens. |
#menu-toggle input:checked ~ #slide-out-menu | |
---|---|
Property & Value | Explanation |
|
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 |
|
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. |
|
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 |
|
Fixes the element relative to the viewport, even when the page is scrolled. |
|
Positions the top edge of the element at 0 pixels from the top of the viewport. |
|
Adds 10 pixels of padding to the content inside the element, creating space between the content and the edges of the sidebar. |
|
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. |
|
Sets the line height of the text inside the element to the height of a single line of text. |
|
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. |
|
Sets the font size of the text inside the element to 1.2 times the default font size. |
|
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 |
|
Adds 20 pixels of padding to the bottom of each list item in the sidebar element. |
#sidebar .nav-link | |
---|---|
Property & Value | Explanation |
|
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. |
|
Removes the underline from the text decoration of the .nav-link elements. |
|
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. |
|
Specifies a smooth transition effect for the color change, lasting 300ms and using an ease timing function. |
#sidebar a:hover | |
---|---|
Property & Value | Explanation |
|
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.