A useful headless component (hook) that gives you all the functions you need to create a multi-level menu using your own components!
- 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).
yarn add react-headless-nested-menu
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);
- Improve documentation.
- Add more example.
- Add tests.
- Use popper for positioning menus.