Hulya Karakaya A frontend developer interested in open source and building amazing websites. I believe in building through collaboration and contribution.

How to create a context menu in React

6 min read 1840

According to Wikipedia, a context menu (also called right-click menu) is a menu in a graphical user interface (GUI) that appears upon user interaction, such as a right-click mouse operation. A context menu offers a limited set of choices that are available in the current state, or context, of the operating system or application to which the menu belongs.

If you right-click on your browser while visiting a website, you can see your OS’s native context menu. You can save, print, create a QR code for the page, and much more. You should also see different options depending on where you are clicking on the page; if you highlight text, you can see options like copy, paste, and cut.

You may also see some customized context menus, like on email or list applications, and collaboration apps like Trello and Notion. These right-click menus give users more options while they are using the app.

In this post, we will explore how you can create a right-click menu in React, shortcuts to activate right-click menus, how to create a custom context menu hook, and some packages if you don’t want to implement them yourself.

You can see the demo of the project below, and check out the full code on Github or the deployed website.

Gif of a context menu demonstration in a React app

Creating a custom right-click menu

To create a right-click menu, we need to use the contextmenu event listener. This event fires when the user attempts to open a context menu. It is typically triggered by clicking the right mouse button, or by pressing the context menu keyboard shortcut.

Creating a keyboard shortcut for a context menu

The keyboard shortcut for a context menu on Windows is Shift+F10. On Mac, you can use Ctrl+click or right+click; there doesn’t seem to be a keyboard-only shortcut.

To create a keyboard shortcut for a context menu on a Mac, first go to System PreferencesAccessibilityPointer ControlAlternate Control Methods✅ Enable alternate pointer actions. To use the shortcut use F12. If you don’t know how to see the function keys because your Mac has a touchbar, check out this quick guide.

Screenshot of pointer control settings on a mac

We made a custom demo for .
No really. Click here to check it out.

Building a context menu

To disable the default right-click menu, we need to use event.preventDefault():

document.addEventListener("contextmenu", (event) => {
        event.preventDefault()
});

Next, we need to capture the x and y coordinates of the click and show the menu where the user clicks on the page. We can get pageX and pageY properties from the event object and apply coordinates to the top and left properties in CSS:

const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });

document.addEventListener("contextmenu", (event) => {
        event.preventDefault()
        setAnchorPoint({ x: event.pageX, y: event.pageY });
});

We are using the useState hook to manage anchor points and will update them when the user right-clicks on the page.

Customizing the context menu

Now we need to show the right-click menu to the user. The default value for the menu is false, so we will be hiding it. When the user right-clicks, set setShow to true:

const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
const [show, setShow] = useState(false);

document.addEventListener("contextmenu", (event) => {
    event.preventDefault()
    setAnchorPoint({ x: event.pageX, y: event.pageY });
    setShow(true);
});

We also need to use the useCallback hook; this will return a memoized version of the callback that only changes if one of the dependencies has changed. This prevents unnecessary renders. React will keep only one copy of our function.

For this, we need to wrap everything inside a useCallback function and pass the dependencies inside an array:

const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
const [show, setShow] = useState(false);

document.addEventListener(
    "contextmenu",
    useCallback(
      (event) => {
        event.preventDefault();
        setAnchorPoint({ x: event.pageX, y: event.pageY });
        setShow(true);
      },
      [setAnchorPoint, setShow]
    )
  );

Utilizing useEffect

The last thing to take care of is the useEffect hook when we are firing events on the DOM. This hook is used for side effects including API calls and DOM manipulation. When we listen for events, we also need to add a cleanup function.

When we run the code, it will clean up the old state first, then run the updated state. This will remove unnecessary behavior and prevent memory leaking issues.

We can do this by passing in a return function:

const handleContextMenu = useCallback(
    (event) => {
      event.preventDefault();
      setAnchorPoint({ x: event.pageX, y: event.pageY });
      setShow(true);
    },
    [setAnchorPoint, setShow]
  );

useEffect(() => {
    document.addEventListener("contextmenu", handleContextMenu);
} return () => {
    document.removeEventListener("contextmenu", handleContextMenu);
});

Positioning the context menu

To show the menu we need to change the top and left position according to its x and y coordinates. We will put them in our App.js component.

Here is the CSS code for the menu:

.menu {
  font-size: 14px;
  background-color: #fff;
  border-radius: 2px;
  padding: 5px 0 5px 0;
  width: 150px;
  height: auto;
  margin: 0;
/* use absolute positioning  */
  position: absolute;
  list-style: none;
  box-shadow: 0 0 20px 0 #ccc;
  opacity: 1;
  transition: opacity 0.5s linear;
}

To change the top and left position of the menu, we will dynamically update its position according to where the user clicked on the webpage. For this, add inline style to the menu class and only show the menu when we have show set to true; otherwise don’t show anything:

// App.js

function App() {
  const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
  const [show, setShow] = useState(false);

  const handleContextMenu = useCallback(
    (event) => {
      event.preventDefault();
      setAnchorPoint({ x: event.pageX, y: event.pageY });
      setShow(true);
    },
    [setAnchorPoint, setShow]
  );

  useEffect(() => {
    document.addEventListener("contextmenu", handleContextMenu);
    return () => {
      document.removeEventListener("contextmenu", handleContextMenu);
    };
  });

  return (
    <div className="app">
      <h1>Right click somewhere on the page..</h1>
      {show ? (
        <ul
          className="menu"
          style={{
            top: anchorPoint.y,
            left: anchorPoint.x
          }}
        >
          <li>Share to..</li>
          <li>Cut</li>
          <li>Copy</li>
          <li>Paste</li>
          <hr className="divider" />
          <li>Refresh</li>
          <li>Exit</li>
        </ul>
      ) : (
        <> </>
      )}
    </div>
  );
}
export default App;

We also need to hide the menu when the user clicks on any menu items, or tries to click out of it. For this, we will have a conditional check: if the menu is shown, toggle the setShow state to false and hide the menu, otherwise don’t do anything. This function is only updated whenever the show state changes with the useCallback hook.

Like we saw with the contextmenu event, we are again dealing with the DOM, this time with the click event.

Add this inside the useEffect hook and remove the event listener in the return function:

const handleClick = useCallback(() => (show ? setShow(false) : null), [show]);

useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => {
      document.removeEventListener("click", handleClick);
    };
  });

That’s it! Here is the full code:

import "./styles.css";
import { useCallback, useEffect, useState } from "react";

function App() {
  const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
  const [show, setShow] = useState(false); // hide menu

  const handleContextMenu = useCallback(
    (event) => {
      event.preventDefault();
      setAnchorPoint({ x: event.pageX, y: event.pageY });
      setShow(true);
    },
    [setAnchorPoint]
  );

  const handleClick = useCallback(() => (show ? setShow(false) : null), [show]);

  useEffect(() => {
    document.addEventListener("click", handleClick);
    document.addEventListener("contextmenu", handleContextMenu);
    return () => {
      document.removeEventListener("click", handleClick);
      document.removeEventListener("contextmenu", handleContextMenu);
    };
  });

  return (
    <div className="app">
      <h1>Right click somewhere on the page..</h1>
      {show ? (
        <ul
          className="menu"
          style={{
            top: anchorPoint.y,
            left: anchorPoint.x
          }}
        >
          <li>Share to..</li>
          <li>Cut</li>
          <li>Copy</li>
          <li>Paste</li>
          <hr className="divider" />
          <li>Refresh</li>
          <li>Exit</li>
        </ul>
      ) : (
        <> </>
      )}
    </div>
  );
}
export default App;

Creating a custom context menu hook

Until now, we have put all of our code inside App.js. However, React is built on top of components, which means we can keep our code more modular.

Let’s create some components. For our custom context menu, we will create a custom hook, and make use of it inside Menu component. I will name the custom hook useContextMenu.js and return show and anchorPoint from it.

The most important part of our custom hook is the return statement. Here, we return whatever we want another component to have access to. We can return an array or an object.

If you return an array, we can name the returned values whatever we want outside the file. We don’t need to keep the same name as what we have returned:

// useContextMenu.js
import { useEffect, useCallback, useState } from "react";

const useContextMenu = () => {
  const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
  const [show, setShow] = useState(false);

  const handleContextMenu = useCallback(
    (event) => {
      event.preventDefault();
      setAnchorPoint({ x: event.pageX, y: event.pageY });
      setShow(true);
    },
    [setShow, setAnchorPoint]
  );

  const handleClick = useCallback(() => (show ? setShow(false) : null), [show]);

  useEffect(() => {
    document.addEventListener("click", handleClick);
    document.addEventListener("contextmenu", handleContextMenu);
    return () => {
      document.removeEventListener("click", handleClick);
      document.removeEventListener("contextmenu", handleContextMenu);
    };
  });
  return { anchorPoint, show };
};

export default useContextMenu;

We will access our custom hook inside the Menu component and pass the Menu component inside the App.js file:

// Menu.js

import useContextMenu from "./useContextMenu";

const Menu = () => {
  const { anchorPoint, show } = useContextMenu();

  if (show) {
    return (
      <ul className="menu" style={{ top: anchorPoint.y, left: anchorPoint.x }}>
        <li>Share to..</li>
        <li>Cut</li>
        <li>Copy</li>
        <li>Paste</li>
        <hr />
        <li>Refresh</li>
        <li>Exit</li>
      </ul>
    );
  }
  return <></>;
};

export default Menu;

Now, our App.js is rendering a Menu component and it looks much simpler:

// App.js

import "./styles.css";
import Menu from "./Menu";

function App() {
  return (
    <div className="app">
      <h1>Right click somewhere on the page..</h1>
      <Menu />
    </div>
  );
}
export default App;

Adding Feather icons to the menu

Finally, I have used the react-feather package to add Feather icons to the project.

All you need is import the package with npm install react-feather. Import it at the top of your project with import * as Icon from "react-feather"; and access the icons like this:
<Icon.Share size={20} /> .

Conclusion and considerations

There are still other options if you don’t want to implement a custom context menu yourself. One of them is Material UI. Material UI is a React UI framework, and it allows you to create different types of menus along with context menus.

Another option is the react-menu package. It offers unlimited levels of submenus, supports radio and checkbox menu items, supports context menus, and adheres to WAI-ARIA Authoring Practices.

If you are creating your own custom context menu, be sure to think about mobile interaction. Users may not be able to right-click if they are using a mobile phone. That’s why you may need to think twice about why you really need a custom context menu. It can cause some bad experiences if the user just wants to see the default menu.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Hulya Karakaya A frontend developer interested in open source and building amazing websites. I believe in building through collaboration and contribution.

Leave a Reply