Hamburger Menu with a Side of React Hooks and Styled Components

Avatar of Maks Akymenko
Maks Akymenko on (Updated on )

We all know what a hamburger menu is, right? When the pattern started making its way into web designs, it was both mocked and applauded for its minimalism that allows main menus to be tucked off screen, particularly on mobile where every pixel of space counts.

CSS-Tricks is all about double the meat.

Love ‘em or hate ‘em, hamburger menus are here and likely will be for some time to come. The problem is how to implement them. Sure, they look simple and straightforward, but they can be anything but. For example, should they be paired with a label? Are they more effective on the left or right side of the screen? How do we tackle closing those menus, whether by click or touch? Should the icon be an SVG, font, Unicode character, or pure CSS? What about a meatless option?

I wanted to build one of those but failed to find a simple solution. Most solutions are based on libraries, like reactjs-popup or react-burger-menu. They are great, but for more complex solutions. What about the core use case of a three-line menu that simply slides a panel out from the side of the screen when it’s clicked, then slides the panel back in when it’s clicked again?

I decided to build my own simple hamburger with sidebar. No pickles, onions or ketchup. Just meat, bun, and a side of menu items.

Are you ready to create it with me?

Here’s what we’re making

See the Pen
Burger menu with React hooks and styled-components
by Maks Akymenko (@maximakymenko)
on CodePen.

We’re building use React for this tutorial because it seems like a good use case for it: we get a reusable component and a set of hooks we can extend to handle the click functionality.

Spin up a new React project

Let’s spin up a new project using create-react-app, change to that folder directory and add styled-components to style the UI:

npx create-react-app your-project-name
cd your-project-name
yarn add styled-components

Add basic styles

Open the newly created project in your favorite code editor and start adding basic styles using styled-components. In your src directory, create a file called global.js. It will contain styles for the whole app. You can write your own or just copy what I ended up doing:

// global.js
import { createGlobalStyle } from 'styled-components';

export const GlobalStyles = createGlobalStyle`
  html, body {
    margin: 0;
    padding: 0;
  }
  *, *::after, *::before {
    box-sizing: border-box;
  }
  body {
    align-items: center;
    background: #0D0C1D;
    color: #EFFFFA;
    display: flex;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
    height: 100vh;
    justify-content: center;
    text-rendering: optimizeLegibility;
  }
  `

This is only a part of global styles, the rest of it you can find here.

The CreateGlobalStyle function is generally used for creating global styles that are exposed to the whole app. We’ll import it so we have access to these styles as we go.

The next step is to add a theme file that holds all our variables. Create a theme.js file in the src directory and add following:

// theme.js
export const theme = {
  primaryDark: '#0D0C1D',
  primaryLight: '#EFFFFA',
  primaryHover: '#343078',
  mobile: '576px',
}

Add layout, menu and hamburger components 🍔

Go to your App.js file. We’re going to wipe everything out of there and create the main template for our app. Here’s what I did. You can certainly create your own.

// App.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { GlobalStyles } from './global';
import { theme } from './theme';

function App() {
  return (
    <ThemeProvider theme={theme}>
      <>
        <GlobalStyles />
        <div>
          <h1>Hello. This is burger menu tutorial</h1>
          <img src="https://image.flaticon.com/icons/svg/2016/2016012.svg" alt="burger icon" />
          <small>Icon made by Freepik from www.flaticon.com</small>
        </div>
      </>
    </ThemeProvider>
  );
}
export default App;

Don’t forget to add the line with the small tag. That’s how we credit flaticon.comhttp://flaticon.com) authors for the provided icon.

Here’s what we’ve got up to this point:

Let me explain a little bit. We imported ThemeProvider, which is a wrapper component that uses the Context API behind the scenes to make our theme variables available to the whole component tree.

We also imported our GlobalStyles and passed them as a component to our app, which means that our application now has access to all global styles. As you can see, our GlobalStyles component is inside ThemeProvider which means we can already make some minor changes in it.

Go to global.js and change the background and color properties to use our defined variables. This helps us implement a theme rather than using fixed values that are difficult to change.

// global.js
background: ${({ theme }) => theme.primaryDark};
color: ${({ theme }) => theme.primaryLight};

We destructure our theme from props. So, instead of writing props.theme each time, we’re using a bunch of brackets instead. I’ll repeat myself: the theme is available because we’ve wrapped our global styles with ThemeProvider.

Create Burger and Menu components

Create a components folder inside the src directory and add two folders in there: Menu and Burger, plus an index.js file.

index.js will be used for one purpose: allow us to import components from one file, which is very handy, especially when you have a lot of them.

Now let’s create our components. Each folder will contain three files.

What’s up with all the files? You’ll see the benefit of a scalable structure soon enough. It worked well for me in a couple of projects, but here is good advice how to create scalable structure.

Go to the Burger folder and create Burger.js for our layout. Then add Burger.styled.js, which will contain styles, and index.js, which will be exporting the file.

// index.js
export { default } from './Burger';

Feel free to style burger toggle in a way you want, or just paste these styles:

// Burger.styled.js
import styled from 'styled-components';

export const StyledBurger = styled.button`
  position: absolute;
  top: 5%;
  left: 2rem;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  width: 2rem;
  height: 2rem;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0;
  z-index: 10;
  
  &:focus {
    outline: none;
  }
  
  div {
    width: 2rem;
    height: 0.25rem;
    background: ${({ theme }) => theme.primaryLight};
    border-radius: 10px;
    transition: all 0.3s linear;
    position: relative;
    transform-origin: 1px;
  }
`;

The transform-origin property will be needed later to animate the menu it toggles between open and closed states.

After adding the styles, go to Burger.js and add the layout:

// Burger.js
import React from 'react';
import { StyledBurger } from './Burger.styled';

const Burger = () => {
  return (
    <StyledBurger>
      <div />
      <div />
      <div />
    </StyledBurger>
  )
}

export default Burger;

After that look at the left top corner. Do you see it?

Time to do the same with the Menu folder:

// Menu -> index.js
export { default } from './Menu';

// Menu.styled.js
import styled from 'styled-components';

export const StyledMenu = styled.nav`
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: ${({ theme }) => theme.primaryLight};
  height: 100vh;
  text-align: left;
  padding: 2rem;
  position: absolute;
  top: 0;
  left: 0;
  transition: transform 0.3s ease-in-out;
  
  @media (max-width: ${({ theme }) => theme.mobile}) {
    width: 100%;
  }

  a {
    font-size: 2rem;
    text-transform: uppercase;
    padding: 2rem 0;
    font-weight: bold;
    letter-spacing: 0.5rem;
    color: ${({ theme }) => theme.primaryDark};
    text-decoration: none;
    transition: color 0.3s linear;
    
    @media (max-width: ${({ theme }) => theme.mobile}) {
      font-size: 1.5rem;
      text-align: center;
    }

    &:hover {
      color: ${({ theme }) => theme.primaryHover};
    }
  }
`;

Next, let’s add the layout for the menu items that are revealed when clicking on our burger:

// Menu.js
import React from 'react';
import { StyledMenu } from './Menu.styled';

const Menu = () => {
  return (
    <StyledMenu>
      <a href="/">
        <span role="img" aria-label="about us">&#x1f481;&#x1f3fb;&#x200d;&#x2642;&#xfe0f;</span>
        About us
      </a>
      <a href="/">
        <span role="img" aria-label="price">&#x1f4b8;</span>
        Pricing
        </a>
      <a href="/">
        <span role="img" aria-label="contact">&#x1f4e9;</span>
        Contact
        </a>
    </StyledMenu>
  )
}
export default Menu;

We’ve got nice emojis here, and best practice is to make them accessible by wrapping each one in a span and adding a couple of properties: role="img" and aria-label="your label". You can read more about it here.

Time to import our new components into our App.js file:

// App.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { GlobalStyles } from './global';
import { theme } from './theme';
import { Burger, Menu } from './components';

// ...

Let’s see, what we’ve got:

Take a look at this nice navigation bar! But we’ve got one issue here: it’s opened, and we want it initially to be closed. We only need to add one line to Menu.styled.js fix it:

// Menu.styled.js
transform: translateX(-100%);

We are well on our way to calling this burger cooked! But first…

Adding open and close functionality

We want to open the sidebar when clicking the hamburger icon, so let’s get to it. Open App.js and add some state to it. We will use the useState hook for it.

// App.js
import React, { useState } from 'react';

After you import it, let’s use it inside the App component.

// App.js
const [open, setOpen] = useState(false);

We set the initial state to false, because our menu should be hidden when the application is rendered.

We need both our toggle and sidebar menu to know about the state, so pass it down as a prop to each component. Now your App.js should look something like this:

// App.js
import React, { useState } from 'react';
import { ThemeProvider } from 'styled-components';
import { GlobalStyles } from './global';
import { theme } from './theme';
import { Burger, Menu } from './components';

function App() {
  const [open, setOpen] = useState(false);
  return (
    <ThemeProvider theme={theme}>
      <>
        <GlobalStyles />
        <div>
          <h1>Hello. This is burger menu tutorial</h1>
          <img src="https://media.giphy.com/media/xTiTnwj1LUAw0RAfiU/giphy.gif" alt="animated burger" />
        </div>
        <div>
          <Burger open={open} setOpen={setOpen} />
          <Menu open={open} setOpen={setOpen} />
        </div>
      </>
    </ThemeProvider>
  );
}
export default App;

Notice that we’re wrapping our components in a div. This will be helpful later when we add functionality that closes the menu when clicking anywhere on the screen.

Handle props in the components

Our Burger and Menu know about the state, so all we need to do is to handle it inside and add styles accordingly. Go to Burger.js and handle the props that were passed down:

// Burger.js
import React from 'react';
import { bool, func } from 'prop-types';
import { StyledBurger } from './Burger.styled';
const Burger = ({ open, setOpen }) => {
  return (
    <StyledBurger open={open} onClick={() => setOpen(!open)}>
      <div />
      <div />
      <div />
    </StyledBurger>
  )
}
Burger.propTypes = {
  open: bool.isRequired,
  setOpen: func.isRequired,
};
export default Burger;

We destructure the open and setOpen props and pass them to our StyledBurger to add styles for each prop, respectively. Also, we add the onClick handler to call our setOpen function and toggle open prop. At the end of the file, we add type checking, which is considered a best practice for aligning arguments with expected data.

You can check whether it works or not by going to your react-dev-tools. Go to Components tab in your Chrome DevTools and click on Burger tab.

Now, when you click on your Burger component, (don’t mix it up with the tab), you should see, that your open checkbox is changing its state.

Go to Menu.js and do almost the same, although, here we pass only the open prop:

// Menu.js
import React from 'react';
import { bool } from 'prop-types';
import { StyledMenu } from './Menu.styled';
const Menu = ({ open }) => {
  return (
    <StyledMenu open={open}>
      <a href="/">
        <span role="img" aria-label="about us">&#x1f481;&#x1f3fb;&#x200d;&#x2642;&#xfe0f;</span>
        About us
      </a>
      <a href="/">
        <span role="img" aria-label="price">&#x1f4b8;</span>
        Pricing
        </a>
      <a href="/">
        <span role="img" aria-label="contact">&#x1f4e9;</span>
        Contact
        </a>
    </StyledMenu>
  )
}
Menu.propTypes = {
  open: bool.isRequired,
}
export default Menu;

Next step is to pass open prop down to our styled component so we could apply the transition. Open Menu.styled.js and add the following to our transform property:

transform: ${({ open }) => open ? 'translateX(0)' : 'translateX(-100%)'};

This is checking if our styled component open prop is true, and if so, it adds translateX(0) to move our navigation back on the screen. You can already test it out locally!

Wait, wait, wait!

Did you notice something wrong when checking things out? Our Burger has the same color as the background color of our Menu, which make them blend together. Let’s change that and also animate the icon a bit to make it more interesting. We’ve got the open prop passed to it, so we can use that to apply the changes.

Open Burger.styled.js and write the following:

// Burger.styled.js
import styled from 'styled-components';
export const StyledBurger = styled.button`
  position: absolute;
  top: 5%;
  left: 2rem;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  width: 2rem;
  height: 2rem;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0;
  z-index: 10;

  &:focus {
    outline: none;
  }

  div {
    width: 2rem;
    height: 0.25rem;
    background: ${({ theme, open }) => open ? theme.primaryDark : theme.primaryLight};
    border-radius: 10px;
    transition: all 0.3s linear;
    position: relative;
    transform-origin: 1px;

    :first-child {
      transform: ${({ open }) => open ? 'rotate(45deg)' : 'rotate(0)'};
    }

    :nth-child(2) {
      opacity: ${({ open }) => open ? '0' : '1'};
      transform: ${({ open }) => open ? 'translateX(20px)' : 'translateX(0)'};
    }

    :nth-child(3) {
      transform: ${({ open }) => open ? 'rotate(-45deg)' : 'rotate(0)'};
    }
  }
`;

This is a big chunk of CSS, but it makes the animation magic happen. We check if the open prop is true and change styles accordingly. We rotate, translate, then hide the menu icon’s lines while changing color. Beautiful, isn’t it?

Okay, folks! By now, you should know how to create a simple hamburger icon and menu, that incorporates responsiveness and smooth animation. Congratulations!

But there’s one last thing we ought to account for…

Close the menu by clicking outside of it

This part seems like a small bonus, but it’s a big UX win because it allows the user to close the menu by clicking anywhere else on the page. This helps the user avoid having to re-locate the menu icon and clicking exactly on it.

We’re going to put more React hooks to use to make this happen! Create a file in the src directory, called hooks.js and open it. For this one, we’re gonna turn to the useEffect hook, which was introduced in React 18.

// hooks.js
import { useEffect } from 'react';

Before we write the code, let’s think about the logic behind this hook. When we click somewhere on the page, we need to check whether the clicked element is our current element (in our case, that is the Menu component) or if the clicked element contains the current element (for instance, our div that wraps our menu and hamburger icon). If so, we don’t do anything, otherwise, we call a function, that we’ll name handler.

We are going to use ref to check the clicked element, and we will do so every time someone clicks on the page.

// hooks.js
import { useEffect } from 'react';

export const useOnClickOutside = (ref, handler) => {
  useEffect(() => {
    const listener = event => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };
    document.addEventListener('mousedown', listener);
    return () => {
      document.removeEventListener('mousedown', listener);
    };
  },
  [ref, handler],
  );
};

Don’t forget to return the function from useEffect. That’s so-called “clean up” and, basically, it stands for removing an event listener when the component unmounts. It is the replacement of componentWillUnmount lifecycle.

Now let’s hook up the hook

We’ve got our hook ready, so it’s time to add it to the app. Go to the App.js file, and import two hooks: the newly created useOnClickOutside and also useRef. We’ll need the latter to get a reference to the element.

// App.js
import React, { useState, useRef } from 'react';
import { useOnClickOutside } from './hooks';

To get access these in the current element, we need to get access to the DOM node. That’s where we use useRef, also, the name node perfectly reflects the point of this variable.

From there, we pass the node as a first argument. We’ll pass the function that closes our menu as a second argument.

// App.js
const node = useRef(); 
useOnClickOutside(node, () => setOpen(false));

Lastly, we need to pass our ref to the DOM element. In our case, it will be div, that holds the Burger and Menu components:

// App.js
<div ref={node}>
  <Burger open={open} setOpen={setOpen} />
  <Menu open={open} setOpen={setOpen} />
</div>

Your App.js should look similar to this:

// App.js
import React, { useState, useRef } from 'react';
import { ThemeProvider } from 'styled-components';
import { useOnClickOutside } from './hooks';
import { GlobalStyles } from './global';
import { theme } from './theme';
import { Burger, Menu } from './components';
function App() {
  const [open, setOpen] = useState(false);
  const node = useRef();
  useOnClickOutside(node, () => setOpen(false));
  return (
    <ThemeProvider theme={theme}>
      <>
        <GlobalStyles />
        <div>
          <h1>Hello. This is burger menu tutorial</h1>
          <img src="https://media.giphy.com/media/xTiTnwj1LUAw0RAfiU/giphy.gif" alt="animated burger" />
        </div>
        <div ref={node}>
          <Burger open={open} setOpen={setOpen} />
          <Menu open={open} setOpen={setOpen} />
        </div>
      </>
    </ThemeProvider>
  );
}
export default App;

Check this out! It works as expected, and it’s fully functional and responsive.

Congratulations, everyone! You did a great job! Happy coding!

View on GitHub