Build a context menu with React and TypeScript

Learn how to use custom hooks and reusable React components to build a context menu.
October 01, 2022React
6 min read

Creating your own right-click context menu in React is easy. Using your own context menu means you'll need one less npm package to install in your project, making it more lightweight. Building a custom context menu with React is also a good front-end programming exercise for practice.

What is a context menu?

A context menu is also called a right-click menu. It's a menu that appears when a user right-clicks on the screen. It offers a set of options that are available to the user in the current context of the application to which it belongs. That's why it's called a context menu.

Right-clicking within a browser tab will show your operating system's native context menu. One context menu that we are all familiar with is the one that shows up when we right-click after copying text. This context menu provides us with options such as Cut, Copy, and Paste.

A context menu hook

Let's start by building a custom useContextMenu hook that we'll store in /hooks/useContextMenu.ts.

This hook will be responsible for implementing all the logic required for a context menu. This means that the context menu component that we'll build later can have the single responsibility of displaying the context menu. It won't have to worry about implementing any logic.

The useContextMenu hook will:

  • Register and de-register event listeners.
  • Keep track of the right-click mouse position.
  • Keep track of whether the context menu is shown or hidden.

We'll listen to the contextmenu event to capture right-clicks. We'll also to listen to click events to know when to close our context menu.

useContextMenu.ts
import { useEffect, useCallback, useState } from 'react';

type AnchorPoint = {
  x: number;
  y: number;
};

const useContextMenu = () => {
  const [anchorPoint, setAnchorPoint] = useState<AnchorPoint>({ x: 0, y: 0 });
  const [isShown, setIsShow] = useState(false);

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

  const handleClick = useCallback(() => {
    if (isShown) {
      setIsShow(false);
    }
  }, [isShown]);

  useEffect(() => {
    document.addEventListener('click', handleClick);
    document.addEventListener('contextmenu', handleContextMenu);

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

  return { anchorPoint, isShown };
};

export { useContextMenu };

In this custom hook, we added the event listeners we needed in a useEffect hook and de-registered them in the useEffect cleanup function.

The handleContextMenu method is called when a user right-clicks anywhere on the screen. It's responsible for saving the right-click anchor point of the user so that we know where exactly we need to display the context menu. This method is also responsible for toggling the isShown status of the context menu to true to make it visible.

The handleClick method is called when a user left-clicks anywhere on the screen. It's responsible for closing the context menu. If the isShown status of the context menu is set to true because it's visible, then this method will set it to false to hide it.

A context menu component

Let's create a reusable component for our context menu and store it in /components/ContextMenu.tsx.

This component will make use of the useContextMenu hook that we just created in order to retrieve the anchorPoint and the isShown status.

ContextMenu.tsx
import * as React from 'react';
import { useContextMenu } from '../../hooks/useContextMenu';

import styles from './ContextMenu.module.css';

type Props = {
  items: Array<string>;
  onClick: (item: string) => void;
};

const ContextMenu = ({ items }: Props) => {
  const { anchorPoint, isShown } = useContextMenu();

  if (!isShown) {
    return null;
  }

  return (
    <ul
      className={styles.ContextMenu}
      style={{ top: anchorPoint.y, left: anchorPoint.x }}
    >
      {items.map((item) => (
        <li key={item} onClick={() => onClick(item)}>{item}</li>
      ))}
    </ul>
  );
};

export { ContextMenu };

If the context menu is not being shown, then the ContextMenu component simply returns null. However, if the context menu is being shown, the ContextMenu component renders it at the precise location provided by the coordinates of the anchorPoint.

The ContextMenu component receives a list configurable menu items, making it a reusable component for many different types of context menus.

Clicking on a context menu item triggers an onClick event on the ContextMenu component. This event passes along the name of the item that was clicked.

Styling the context menu

The next step is to add the CSS styles for the ContextMenu component. To do so, we'll make use of CSS Modules and create a ContextMenu.module.css file.

If you recall, we've already imported this CSS file in the ContextMenu component using the following import.

import styles from './ContextMenu.module.css';

Let's make use of the .ContextMenu CSS class to add styling to the context menu. This class was assigned to the <ul> element in the ContextMenu component via className={styles.ContextMenu}. It's important that we use absolute positioning for this CSS class.

Also, let's use the not(:last-child) CSS rule on the <li> element to add bottom margins on all context menu items, except for the last one.

ContextMenu.module.css
.ContextMenu {
  position: absolute;
  background: #eee;
  border: 1px solid #ccc;
  border-radius: 0.3rem;
  padding: 1rem;
  width: 8rem;
  list-style: none;
  margin: 0;
}

.ContextMenu li {
  cursor: pointer;
}

.ContextMenu li:not(:last-child) {
  margin: 0 0 1rem;
}

Making use of the context menu

Let's complete this exercise by making use of the ContextMenu component in our App component. This will allow us to test the context menu that we've built.

We'll pass a list of menu items to our ContextMenu component to populate the context menu. We'll also provide a function to the onClick handler of the ContextMenu component so that we can be notified when a context menu item is clicked.

App.tsx
import * as React from 'react';
import { ContextMenu } from './components';

export default function App() {
  const items = ['Cut', 'Copy', 'Paste'];

  const onClick = (item: string) => {
    alert(`${item} was clicked.`);
  };

  return (
    <div>
      <h1>React Context Menu</h1>
      <p>Right-click anywhere to see the context menu</p>
      <p>Left-click to close the context menu</p>
      <ContextMenu items={items} onClick={onClick} />
    </div>
  );
}

Right-clicking anywhere on the page now shows a context menu with the menu items that we provided. Once the context menu is shown, left-clicking anywhere on the page closes it.

React context menu demo

Conclusion

In this article, we learned how to build our own context menu with a React custom hook and a reusable React component.

You can now implement this context menu in your next application or in a shared component library that you are building.

New
Be React Ready

Learn modern React with TypeScript.

Learn modern React development with TypeScript from my books React Ready and React Router Ready.