-- online Search!

Creating a Smooth Sliding Menu

by kirupa   | filed under Learning React

In UIs today, sliding menus are all the rage. All the cool kids are building them, and your friends just can't get enough of them! Anyway, these menus are basically off-screen elements that slide into view when you click or tap on something. That something could be an arrow, a hamburger icon, or something else that indicates a menu will appear.

To see a sliding menu in action, click or tap on the blue circle in the following example (or open in a new window):

You'll see a yellow-colored menu with some navigation links smoothly slide in. If you click on a navigation link or anywhere in the yellow colored region inside that menu, the menu will slide back (really smoothly again, of course!) and the content behind it will reappear. You can also press the Esc key to close the menu. Let's look at how to create all of this using React.

Onwards!

Learning React by Kirupa Chinnathambi

OMG! A React Book Written by Kirupa?!!

To kick your React skills up a few notches, everything you see here and more (with all its casual clarity!) is available in both paperback and digital editions.

BUY ON AMAZON

Note: For a Non-React Solution

If you are looking to create this menu using just plain JavaScript without any React wizardry, this Creating a Smooth Sliding Menu tutorial has you covered.

No Animation Library Needed

You may have seen older versions of this tutorial that used an animation library such as react-motion. That library is no longer maintained, and we don't need it. The sliding is done entirely by CSS. All React does is toggle a class at the right time. This is how most animations on the web are done. If you eventually need something fancier, like animating elements as they leave the page or physics-based springs, take a look at a library like Motion.

How the Sliding Menu Works

Before we jump into the code, let's take a few moments to better understand how exactly our sliding menu works. Starting at the very top, we have our page that displays some content:

A page with a blue circle button and a short list of text

When you decide to bring up the menu (clicking/tapping on the blue circle as is the case in our example), the menu magically slides into view:

A yellow menu sliding in from the left to cover the page content

The way this sliding menu works is not as crazy as it seems. The menu is never truly nonexistent. It is simply hidden outside of the view. To see what that looks like, take a look at the following diagram:

The menu sitting just to the left of the visible page, outside of view

Just to the left of the content that we see, we have our menu...patiently hiding until it is called upon. The way we do that is by shifting the menu as far left as we can until it is fully out of view. Figuring out how far to shift is easy. Our menu's size is the same as our browser's window (aka the viewport) size, for we want the menu to fully cover up whatever is shown. Given that detail, we just shift the menu left by the browser's width. One way of doing that might be by using some CSS that looks as follows:

#flyoutMenu {
  position: fixed;
  left: 0;
  top: 0;
  transform: translate3d(-100vw, 0, 0);

  width: 100vw;
  height: 100vh;
}

We set our menu's position to fixed. This single change gives our menu a whole lot of magical capabilities. For starters, this ensures normal layout rules no longer apply to it. We can position our menu anywhere we want using normal x and y values, and the menu won't shift away from where we have it positioned. If all of that isn't awesome enough, our menu won't even display a scrollbar if we happen to hide it somewhere off screen.

All of this is a good thing, for the way we hide our menu off screen is by setting our menu's left and top properties to 0 and setting our menu's transform property to a translate3d method with a horizontal value of -100vw. The negative value ensures we shift the menu left by the amount equivalent to our browser window's width. While not related to position directly, the size of our menu plays an important role as well. That is why in this CSS snippet, we have the width and height properties set with values of 100vw and 100vh respectively to ensure our menu's size is the same as our browser window's size.

What is this vw and vh unit?

If you've never seen the vw and vh units before, they stand for viewport width (vw) and viewport height (vh). They are a bit similar to percentage values. Each unit is 1/100th the width or height of your viewport (what we've been simply calling the browser window). For example, a value of 100vw means that its value is the full width of our browser window. Similarly, 100vh refers to a value that is the full height of our browser window.

When the menu is called upon to slide into view, we slide the menu right until its horizontal position is the same as our browser window origin. This is an easy change from what we already have. We simply set our transform property's translate3d method and set the horizontal position to a value of 0:

transform: translate3d(0, 0, 0);

This change ensures our menu is shifted right from being hidden off-screen (with a horizontal translate value of -100vw) and is now visible. When our menu needs to disappear, we can translate it back:

transform: translate3d(-100vw, 0, 0);

The biggest thing we haven't spoken about is the animation that makes the sliding look cool. This is done using a simple CSS transition that animates the transform property:

transition: transform .3s cubic-bezier(0, .52, 0, 1);

If you are not familiar with CSS transitions, it's a very simple concept to wrap your brain around. While I won't explain it here, take a few moments and read through the short Introduction to CSS Transitions article for an overview.

OK. What have we covered so far? What we've done in this section is take a bird's-eye view of how our sliding menu works. Notice that React hasn't shown up yet. That is intentional! React's job here is small: it decides whether the menu is currently in its hidden or shown state, and the browser takes care of the actual sliding. There are a few details that we haven't looked at, but that is something we'll address in the next couple of sections as part of actually building this menu.

Setting Up the Sliding Menu

Now that you have a basic idea about how our sliding menu works, let's turn all of that theoretical knowledge into some sweet JSX and code. The first thing we are going to do is look at our example in terms of the individual components that will make it up.

At the very top, we have our MenuContainer component:

A page with a round button and a list of text lines, labeled MenuContainer

This component is responsible for doing non-visual things like managing state, hosting our Menu & MenuButton components, and displaying some of the initial text. The bird's-eye view would look a bit like this:

MenuContainer at the top with two child components below it: MenuButton and Menu

What we are going to do in the next few sections is start creating these components and get the example up and running.

Getting Started

We'll use Vite to create our project. If you aren't familiar with how to do that, check out this tutorial that walks you through the details of creating and working with React projects. From your terminal, run the following command:

npm create vite@latest slidingmenu -- --template react

Once your project has been created and its dependencies installed, navigate into the slidingmenu folder. We want to start from a blank slate. Inside the src folder, delete App.jsx, App.css, and the assets folder. We'll re-create the necessary pieces ourselves in a few moments. We can leave the index.html file in the root of our project alone, other than changing the title element to say Sliding Menu in React. It already has a div element with an id of root that all of our React components will eventually render their output into.

Next, replace the contents of the main.jsx file in our src folder with the following. This file is responsible for teeing things up in our code:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import MenuContainer from "./MenuContainer.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <MenuContainer />
  </StrictMode>
);

The render call here is responsible for displaying the output of our MenuContainer component into the root div element. In our import statements, besides pulling in React, we are referencing index.css and our MenuContainer component. That is all there is to our main.jsx file.

What we are going to do next is open the index.css file in our src folder and replace everything inside it with the following two style rules. This gets the page's basic styling defined:

body {
  background-color: #EEE;
  font-family: sans-serif;
  font-size: 20px;
  padding: 25px;
  margin: 0;
}

#root li {
  margin-bottom: 10px;
}

There isn't much to say about these style rules, so the last thing we will do for getting our initial app set up is create our MenuContainer component. Create a file called MenuContainer.jsx in the src folder and add the following into it:

function MenuContainer() {
  return (
    <div>
      <p>Can you spot the item that doesn't belong?</p>
      <ul>
        <li>Lorem</li>
        <li>Ipsum</li>
        <li>Dolor</li>
        <li>Sit</li>
        <li>Bumblebees</li>
        <li>Aenean</li>
        <li>Consectetur</li>
      </ul>
    </div>
  );
}

export default MenuContainer;

Make sure to save the changes you made to all of your files, and test your app out (using npm run dev) to ensure our initial setup of our app works fine. If everything worked out properly, open the address that Vite displays in your browser and you'll see something that looks as follows:

A gray page with the question Can you spot the item that doesn't belong? followed by a bulleted list

There is no menu to slide or button to press. Those are both things we'll add in the next couple of sections.

Showing and Hiding the Menu

With the initial setup out of the way, it's time for the fun part: actually building the menu! The way our menu is shown or hidden is as follows:

  1. When you click on a button, the menu slides into view
  2. When you click anywhere on the menu, the menu slides out of view

This means that there are a few things going on that we need to account for. There is some state we will need to maintain to keep track of whether the menu is hidden or shown. This state needs to be something we update from both the button and the menu, for clicking on either will toggle whether the menu is visible or not. We need our state to live in a common location that both the menu and the button can access. That common location will be inside our MenuContainer component (this is the "lifting state up" idea we saw in Dealing with State), so let's add the code relating to our state logic.

In our MenuContainer.jsx file, add the following highlighted lines:

import { useState } from "react";

function MenuContainer() {
  const [visible, setVisible] = useState(false);

  function toggleMenu() {
    setVisible((current) => !current);
  }

  return (
    <div>
      <p>Can you spot the item that doesn't belong?</p>
      <ul>
        <li>Lorem</li>
        <li>Ipsum</li>
        <li>Dolor</li>
        <li>Sit</li>
        <li>Bumblebees</li>
        <li>Aenean</li>
        <li>Consectetur</li>
      </ul>
    </div>
  );
}

export default MenuContainer;

The code we've just added should look like a walk in the park by now. We are storing a state variable called visible that starts out as false, and we create a function called toggleMenu that will be responsible for flipping visible between true and false. Notice that we pass setVisible a function that receives the current value and returns the opposite. This is the safest way to toggle a value based on its previous value.

We have our state and a function that updates it. What we don't have is anything that actually calls it. We'll do that when we create our button and menu components next, for those are the components that will have the click events.

Creating the Button

In your src folder, create two files called MenuButton.jsx and MenuButton.css. Once you've created both of these files, open MenuButton.jsx in your code editor. Inside it, add the following lines of code:

import "./MenuButton.css";

function MenuButton({ toggleMenu, visible }) {
  return (
    <button
      id="roundButton"
      aria-label="Menu"
      aria-expanded={visible}
      aria-controls="flyoutMenu"
      onClick={toggleMenu}
    ></button>
  );
}

export default MenuButton;

Take a moment to see what this code is doing. There isn't a whole lot going on. We define a button element called roundButton, and we associate its onClick event with a prop called toggleMenu. When the button is clicked, whatever function was passed in gets called.

The other attributes are there for people who can't see our blue circle. Our button doesn't have any text, so aria-label gives it a name that a screen reader can announce. The aria-expanded attribute tells assistive technologies whether the thing the button controls is currently open, and aria-controls points to the element it controls (our menu, which we'll create in a moment). Because we are using a real button element, keyboard users can also reach it with the Tab key and activate it with Enter or Space. We get all of that for free. If we had used a div instead, we'd have had to build all of that ourselves.

Before we move on, open MenuButton.css and add the following style rules:

#roundButton {
  background-color: #96D9FF;
  margin-bottom: 20px;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: 10px solid #0065A6;
  transition: all .2s cubic-bezier(0, 1.26, .8, 1.28);
}

#roundButton:hover {
  cursor: pointer;
  border-color: #003557;
  transform: scale(1.2, 1.2);
}

#roundButton:active {
  border-color: #003557;
  background-color: #FFF;
}

#roundButton:focus-visible {
  outline: 3px solid #003557;
  outline-offset: 4px;
}

The last rule, :focus-visible, draws an outline around the button when someone navigates to it with their keyboard. You'll often see people remove focus outlines because they don't like how they look, but that makes keyboard navigation guesswork. Here, we've given it an outline that fits our design instead.

Once you've made this change, it is time to actually use our newly created MenuButton component. Go back to the MenuContainer component and add the following highlighted lines:

import { useState } from "react";
import MenuButton from "./MenuButton.jsx";

function MenuContainer() {
  const [visible, setVisible] = useState(false);

  function toggleMenu() {
    setVisible((current) => !current);
  }

  return (
    <div>
      <MenuButton toggleMenu={toggleMenu} visible={visible} />
      <div>
        <p>Can you spot the item that doesn't belong?</p>
        <ul>
          <li>Lorem</li>
          <li>Ipsum</li>
          <li>Dolor</li>
          <li>Sit</li>
          <li>Bumblebees</li>
          <li>Aenean</li>
          <li>Consectetur</li>
        </ul>
      </div>
    </div>
  );
}

export default MenuContainer;

That import statement at the top is easy to overlook, so make sure it is there. We've also wrapped our existing paragraph and list in a new div, which is why the opening and closing tags of that div are highlighted too. Notice that we are passing in two props. The first is toggleMenu, and its value is the function we defined earlier. This ensures that when you click on the button inside our MenuButton component, the toggleMenu function that lives in our MenuContainer component gets called. This is how a child component gets to change the state of its parent: the parent hands it a function. The second prop is visible, which the button uses for its aria-expanded attribute. If you preview your app now, you'll see the button sitting above our text:

The page now showing a round blue button above the question and list

All of this is great, but our button isn't very useful without a menu to help slide into view. We'll fix that up next.

Creating the Menu

It is time to create our Menu component that will be responsible for all things dealing with the menu! In the same src folder we have been partying in for the past few sections, add one file called Menu.jsx and add another file called Menu.css. Inside Menu.jsx, add the following contents:

import "./Menu.css";

function Menu({ toggleMenu, visible }) {
  return (
    <div
      id="flyoutMenu"
      className={visible ? "show" : ""}
      onClick={toggleMenu}
    >
      <h2><a href="#home">Home</a></h2>
      <h2><a href="#about">About</a></h2>
      <h2><a href="#contact">Contact</a></h2>
      <h2><a href="#search">Search</a></h2>
    </div>
  );
}

export default Menu;

Pay attention to the JSX in the return statement. We have a div element called flyoutMenu with some sample content. When this div is clicked, we call our toggleMenu function (passed in via a prop). Clicks on the links inside the menu bubble up to the div, so those close the menu as well. Next, we set a className value on this element, and the value is either show or an empty string depending on whether the visible prop is true or false. As you may recall, class is a reserved name in JavaScript and you can't use it directly in our JSX. It has to be specified instead as className.

While it may not look like it, the code revolving around className plays a really important role in determining whether your menu is actually visible or not. When we look at our CSS, you'll see why. Speaking of which, open Menu.css and add the following style rules into it:

#flyoutMenu {
  width: 100vw;
  height: 100vh;
  background-color: #FFE600;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1000;
  overflow: auto;
  transform: translate3d(-100vw, 0, 0);
  visibility: hidden;
  transition: transform .3s cubic-bezier(0, .52, 0, 1),
              visibility 0s .3s;
}

#flyoutMenu.show {
  transform: translate3d(0, 0, 0);
  visibility: visible;
  transition: transform .3s cubic-bezier(0, .52, 0, 1),
              visibility 0s 0s;
}

#flyoutMenu h2 a {
  color: #333;
  margin-left: 15px;
  text-decoration: none;
}

#flyoutMenu h2 a:hover {
  text-decoration: underline;
}

@media (prefers-reduced-motion: reduce) {
  #flyoutMenu,
  #flyoutMenu.show {
    transition: none;
  }
}

Most of this CSS deals with how our menu looks, but the showing and hiding of the menu is handled by the #flyoutMenu and #flyoutMenu.show style rules. By default, our menu is shifted off-screen. When the show class is present, which is entirely dependent on the className code we looked at earlier, the menu is shifted into view. The transition property takes care of animating between the two. Pretty cool, right?

There are two extra details in here that are worth calling out. The first is the visibility property. A menu that is shifted off-screen is invisible to our eyes, but the links inside of it can still be reached by tabbing through the page, and screen readers will still read them out. Setting visibility to hidden fixes that. It removes the menu from tab order and from assistive technologies. The tricky part is that we don't want the menu to disappear instantly when it closes, because it would vanish before it finished sliding. That is why the hidden state has visibility 0s .3s in its transition. It says "change visibility in zero seconds, but wait .3 seconds before starting." That matches how long our slide takes, so the menu becomes hidden right when it finishes sliding out. When the menu opens, there is no delay, so it is visible right away.

The second detail is the prefers-reduced-motion media query. Some people turn on a setting in their operating system to say they'd rather not see animations. This is often because motion can cause discomfort. Our query respects that setting by turning off the transition, and the menu simply appears and disappears.

Now, go back to our MenuContainer component and add the following highlighted lines to bring our menu in:

import { useState } from "react";
import MenuButton from "./MenuButton.jsx";
import Menu from "./Menu.jsx";

function MenuContainer() {
  const [visible, setVisible] = useState(false);

  function toggleMenu() {
    setVisible((current) => !current);
  }

  return (
    <div>
      <MenuButton toggleMenu={toggleMenu} visible={visible} />
      <Menu toggleMenu={toggleMenu} visible={visible} />
      <div>
        <p>Can you spot the item that doesn't belong?</p>
        <ul>
          <li>Lorem</li>
          <li>Ipsum</li>
          <li>Dolor</li>
          <li>Sit</li>
          <li>Bumblebees</li>
          <li>Aenean</li>
          <li>Consectetur</li>
        </ul>
      </div>
    </div>
  );
}

export default MenuContainer;

Look at the props we are passing in to Menu. We are giving it the same toggleMenu function the button gets, and the current value of our visible state. Both the button and the menu share the same piece of state, and any change to it will update both of them. Save your changes, and click on the blue circle. Here is what you should see when the menu has finished sliding in:

A fully yellow menu covering the page with the links Home, About, Contact, and Search

Closing the Menu with the Escape Key

There is one more thing we should do. People expect menus and dialogs like this to close when they press the Esc key. Listening for a key press anywhere on the page isn't something we can do with an onClick on one of our elements. We need to listen on the document itself. Reaching outside of React like this is a job for an effect, which we covered in Effects with useEffect. Make the following changes to MenuContainer.jsx:

import { useState, useEffect } from "react";
import MenuButton from "./MenuButton.jsx";
import Menu from "./Menu.jsx";

function MenuContainer() {
  const [visible, setVisible] = useState(false);

  function toggleMenu() {
    setVisible((current) => !current);
  }

  useEffect(() => {
    if (!visible) {
      return;
    }

    function handleKeyDown(e) {
      if (e.key === "Escape") {
        setVisible(false);
      }
    }

    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, [visible]);

  return (
    <div>
      <MenuButton toggleMenu={toggleMenu} visible={visible} />
      <Menu toggleMenu={toggleMenu} visible={visible} />
      <div>
        <p>Can you spot the item that doesn't belong?</p>
        <ul>
          <li>Lorem</li>
          <li>Ipsum</li>
          <li>Dolor</li>
          <li>Sit</li>
          <li>Bumblebees</li>
          <li>Aenean</li>
          <li>Consectetur</li>
        </ul>
      </div>
    </div>
  );
}

export default MenuContainer;

Our effect depends on visible. If the menu isn't open, the effect does nothing, because there is nothing to close. When the menu opens, we add a keydown listener to the document. If the key that was pressed is Escape, we set visible to false. The function we return is the cleanup. React runs it when the menu closes (or when the component goes away) so that we don't leave listeners lying around. Save your changes, open the menu, and press Esc. Our menu slides away!

At this point, we are fully done with all of our coding. Make sure to save all of your changes and ensure the app works just like the example we started off with. Don't nuke this project, though. We will be revisiting this in the next tutorial to look at how React decides when to render our components.

Conclusion

This is one of the first examples we've looked at where we are using React to create something that is a common UI occurrence: a sliding menu. As part of this, we learned more about the interplay between components, such as dealing with events and event handlers, sharing state, and passing functions down as props. We also saw that a smooth animation doesn't need to involve a special library. A bit of state, a class name, and a CSS transition were all we needed. As we look at more examples together, you'll see that there isn't a whole lot more to React than what we've seen here. All that remains is just a clever arrangement and re-arrangement of the same concepts in more complex scenarios. This doesn't mean we are done, though. There is more React to be had and more examples to create and fully understand!

Next tutorial: Avoiding Unnecessary Renders

Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence slop, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.

Your support keeps this site going! 😇

Kirupa's signature!

Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over a bazillion subscribers!

SUBSCRIBE NOW

Creating engaging and entertaining content for designers and developers since 1998.

Follow:

Popular

Loose Ends

:: Copyright KIRUPA 2026 //--