react-headless-nested-menu

A useful headless component (hook) that gives you all the functions you need to create a multi-level menu using your own components!

Usage no npm install needed!

<script type="module">
  import reactHeadlessNestedMenu from 'https://cdn.skypack.dev/react-headless-nested-menu';
</script>

README

React Headless Nested Menu Logo

React Headless Nested Menu

A useful headless component (hook) that gives you all the functions you need to create a multi-level menu using your own components!

Features

  • Only functionality, no need to fight with CSS classes and overrides to customize your menu.
  • Created in TypeScript, so you get types out of the box.
  • Fully configurable behavior (open on click or hover).

React Headless Nested Menu Logo

Installation

yarn add react-headless-nested-menu

Usage

You can import the generated bundle to use the whole library generated by this starter:

import React from "react";
import { useNestedMenu } from "react-headless-nested-menu";

function App() {
  const {
    getToggleButtonProps,
    getMenuProps,
    getItemProps,
    getOpenTriggerProps,
    getCloseTriggerProps,
    getMenuOffsetStyles,
    isOpen,
    isSubMenuOpen,
    toggleMenu
  } = useNestedMenu({
    items
  });

  const [item, setItem] = useState<MenuItem>();

  // your custom function to render items
  const renderItem = (item: MenuItem) => (
    <div
      {...getItemProps(item)}
      className="relative my-1 first:mt-0 last:mb-0"
      {...getOpenTriggerProps("onPointerEnter", item)}
      onClick={(event) => {
        event.stopPropagation();
        setItem(item);
        toggleMenu();
      }}
    >
      <div
        className={classnames(
          "flex flex-row justify-between items-center rounded-lg flex-1 h-8 flex items-center px-2",
          {
            "text-gray-600 hover:text-gray-800 hover:bg-gray-200": !isSubMenuOpen(
              item
            ),
            "text-gray-800 bg-gray-200": isSubMenuOpen(item)
          }
        )}
      >
        {item.label}
        {item.subMenu && <Chevron />}
      </div>

      {/* Only show submenu when there's a submenu & it's open */}
      {item.subMenu && isSubMenuOpen(item) && renderMenu(item.subMenu, item)}
    </div>
  );

  // your custom function to render menus (root menu & sub-menus)
  const renderMenu = (items: Items, parentItem?: MenuItem) => (
    <div
      {...getMenuProps(parentItem)}
      style={{
        position: "absolute",
        ...getMenuOffsetStyles(parentItem)
      }}
      className={classnames(
        "bg-white p-2 shadow-lg rounded-lg select-none border border-gray-100 relative z-10",
        {
          "ms-2": typeof parentItem === "undefined", //for root menu
          "-mt-3": typeof parentItem !== "undefined" //for submenus only
        }
      )}
      {...getCloseTriggerProps("onPointerLeave", parentItem)}
    >
      <div>{items.map((item) => renderItem(item))}</div>

      {/* add hit area */}
      {parentItem && (
        <div
          style={{
            position: "absolute",
            top: -8,
            bottom: -8,
            left: -8,
            right: -8,
            zIndex: -1
          }}
        ></div>
      )}
    </div>
  );

  return (
    <div className="w-64 p-4 rounded-lg flex flex-col ms-4 mt-4">
      <button
        className="text-gray-600 border-2 border-gray-600 rounded-lg h-10 focus:outline-none"
        {...getToggleButtonProps()}
      >
        {item ? item.label : "Open Menu"}
      </button>
      {isOpen && renderMenu(items)}
    </div>
  );
}

const rootElement = document.getElementById("root");
React.render(<App />, rootElement);

To do

  • Improve documentation.
  • Add more example.
  • Add tests.
  • Use popper for positioning menus.

Examples

Credits