Simple Slide-Out Mobile Menu with React Hooks
Jarod Peachey
Posted on April 26, 2020
If you're a front-end developer, I can almost guarantee that you've worked with a mobile menu at some point in your career. If you have, you know first-hand that, sometimes, crafting mobile menus can be a little difficult.
Just this month, I was working on a website of mine, and I came across one of these issues. I wanted to create a mobile menu with a few simple requirements:
- It slides out from under the header
- That's it
Seriously, it's not like I wanted to add 18 different animations, transitions, and effects. I just wanted to make the menu slide out from underneath the header. Simple, right?
Nope.
As I found out, it wasn't as simple as setting the z-index of the menu to be less than that of the header. And trust me, I set the z-index to 999999
just to see what would happen (spoiler: nothing).
After browsing through Stackoverflow, Github, and other forums for way too long, I eventually found a solution. The solution made perfect sense after I found it, but I'm writing this post anyway so others can easily find it as well.
The Structure
For this tutorial, I'm using React with styled-components.
Why?
Because that's what I built it with.
Anyway, I'm going to go through this assuming you already have your app set up, with a header and all that good stuff.
If you don't, go ahead and set up a basic React project.
OK. If you don't already have one, create a header.js
file. I don't care where.
To start, we're going to create a functional component and structure the header and mobile menu.
header.js
import React, { useState } from 'react';
export const Header = () => {
return (
<Wrapper>
<HeaderWrapper id='header'>
<Container>
<Title>Menu Demo</Title>
<MenuToggle>
<RotateContainer>
<span />
<span />
<span />
</RotateContainer>
</MenuToggle>
</Container>
</HeaderWrapper>
<MenuWrapper>
<Menu>
<MenuItem href='/'>Home</MenuItem>
<MenuItem href='/'>About</MenuItem>
<MenuItem href='/'>Contact</MenuItem>
</Menu>
</MenuWrapper>
</Wrapper>
);
};
Since we're using styled-components, each component is named accordingly.
Oh, and remember when I said I spent forever messing with the z-index
of the menu, and it didn't work?
That's because the mobile menu must be a sibling of the header, not a child. It doesn't work if it a child element.
Anyways. After we have the basic structure, we need to style each item.
Styling the menu
Styling the Wrapper
This isn't very exciting, to be honest. It's got three lines of CSS in it.
But, still very important.
Place this styled-component at the bottom of your header.js
file:
const Wrapper = styled.div`
* {
box-sizing: border-box;
}
`;
The box-sizing
rule is needed to size elements with margin and padding properly. However, you most likely won't need this, as your root CSS file probably sets the same rule.
Styling the Header Wrapper
The header wrapper is also optional. If you already have a header on your website (very important, you probably should), then you can keep your current styling.
If not, add a new HeaderWrapper
styled-component to your file:
const HeaderWrapper = styled.header`
padding: 18px 0;
color: white;
position: fixed;
background: tomato;
left: 0;
top: 0;
right: 0;
bottom: auto;
z-index: 999;
`;
There's one specific line of CSS that is very important: z-index: 999;
To display the header over the top of the menu, you need to make sure the header component has a z-index higher than the mobile menu.
Moving on.
Styling the Menu Toggle
I took a little detour on the mobile menu toggle and added some pleasing transitions, using <span>
tags.
At the bottom of your file, add two new styled-components: one for the toggle, and one to handle the rotation of the toggle.
const MenuToggle = styled.div`
z-index: 9999;
width: 30px;
height: 30px;
transform: rotate(0deg);
transition: all 0.25s ease-in;
cursor: pointer;
margin-left: auto;
span {
display: block;
position: absolute;
height: 4px;
width: 100%;
background: white;
border-radius: 9px;
opacity: 1;
left: 0;
transform: rotate(0deg);
transition: ${(props) =>
props.open ? 'all 0.25s ease-in' : 'all 0.25s ease-out'};
}
span:nth-child(1) {
top: ${(props) => (props.open ? 'calc(50% - 2px)' : '10%')};
transform-origin: left center;
}
span:nth-child(2) {
top: ${(props) => (props.open ? 0 : 'calc(50% - 2px)')};
left: ${(props) => (props.open ? 'calc(50% - 2px)' : null)};
width: ${(props) => (props.open ? '4px' : null)};
height: ${(props) => (props.open ? '100%' : null)};
transform-origin: left center;
}
span:nth-child(3) {
top: calc(90% - 4px);
transform-origin: left center;
width: ${(props) => (props.open ? 0 : null)};
opacity: ${(props) => (props.open ? 0 : 1)};
}
`;
const RotateContainer = styled.div`
height: 100%;
width: 100%;
transition: ${(props) => props.open ? 'all 0.25s ease-in-out' : 'all 0.25s ease-in-out'};
transform: ${(props) => (props.open ? 'rotate(-45deg)' : 'none')};
`;
To summarize that, we're adding three <span>
tags inside the MenuToggle
component. The <span>
tags are rotated depending on whether the menu is open or not.
You may notice the variables inside the styled-components: width: ${(props) => (props.open ? 0 : null)};
These are what decide whether or not to show the mobile menu, rotate the toggle, or apply other menu-related styling. We'll be adding that functionality later on.
Styling the Menu
Don't worry, this is the last one, I promise.
The menu styling also depends entirely on your preference, but, once again, there are some important lines of CSS that you need.
const MenuWrapper = styled.div`
position: fixed;
overflow: hidden;
top: ${(props) => (props.open ? '0' : '-100%')};
left: 0;
z-index: 0;
margin-top: 66px;
width: 100%;
transition: ${(props) =>
props.open ? 'all 0.25s ease-out' : 'all 0.6s ease-out'};
box-shadow: 0px 4px 20px -5px #e8e8e8;
padding: 12px;
`;
If you have your own styling, simply add in these 5 lines (the very important ones):
position: fixed;
overflow: hidden;
top: ${(props) => (props.open ? '0' : '-100%')};
left: 0;
z-index: 0;
Adding the functionality
Great! So far, we have nothing useful. We've got a menu that doesn't close, which is probably not the best thing for your website. Luckily, we're not done yet.
To open and close the menu, we need to set an open
state that tells each component whether or not the menu is open.
To do this, we're using the React useState()
hook.
Inside your functional component, add the state, as well as a toggle function that will set the menu to open or closed:
const Header = () => {
const [open, setOpen] = useState(false);
const toggleMenu = () => {
setOpen(!open);
};
...
}
Now that we have our open
state, we need to call the toggleMenu()
function when we click on the menu toggle.
Update your <MenuToggle>
component to look like this:
<MenuToggle onClick={toggleFunction} open={open}>
<RotateContainer open={open}>
<span />
<span />
<span />
</RotateContainer>
</MenuToggle>
Now, when you click on the menu toggle, it should switch from a hamburger menu to an X. The menu doesn't show up yet, because we haven't hooked it up to the open
state yet.
Let's do that now.
Update your <MenuWrapper>
and <Menu>
components:
<MenuWrapper open={open}>
<Menu open={open}>
<MenuItem href='/'>Home</MenuItem>
<MenuItem href='/'>About</MenuItem>
<MenuItem href='/'>Contact</MenuItem>
</Menu>
</MenuWrapper>
And OPEN SESAME! A mobile menu now appears when you click the hamburger menu 🎉 (guess what happens when you click the X)
So, we now have a functioning mobile menu that slides out from underneath the header, as well as a menu toggle that looks pretty sweet!
If you liked this article, please leave a like.
Thanks for reading!
Posted on April 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.