Menu & Menu item
Floating list of contextual actions or links.
Menu / Menu_item Examples
Copy-ready examples with live previews.
Basic usage (controlled)
You own `isOpen`. Toggle it from your trigger and close it from item handlers.
Live preview
Basic
import { useState } from "react";
import Menu from "@/packages/ui/src/_components/menu/Menu";
import Menu_item from "@/packages/ui/src/_components/menu/menu_item/Menu_item";
export default function Example() {
const [open, setOpen] = useState(false);
return (
<Menu
isOpen={open}
position="bottom"
contentBody={
<div className="cssnt-menu__panel">
<Menu_item label="Save" onSelect={() => setOpen(false)} />
<Menu_item label="Share" onSelect={() => setOpen(false)} />
</div>
}
onChange={setOpen}
>
<button type="button" onClick={() => setOpen((v) => !v)}>
Toggle menu
</button>
</Menu>
);
}Rich menu (text + footer)
Use `type="rich"` to enable a header text and an optional footer slot.
Live preview
Rich
import { useState } from "react";
import Menu from "@/packages/ui/src/_components/menu/Menu";
import Menu_item from "@/packages/ui/src/_components/menu/menu_item/Menu_item";
export default function Example() {
const [open, setOpen] = useState(false);
return (
<Menu
isOpen={open}
type="rich"
title="Actions"
position="right"
contentBody={
<div className="cssnt-menu__panel">
<Menu_item label="Cut" trailingText="⌘X" onSelect={() => setOpen(false)} />
<Menu_item label="Copy" trailingText="⌘C" onSelect={() => setOpen(false)} />
<Menu_item label="Paste" disabled onSelect={() => setOpen(false)} />
</div>
}
contentFooter={<div style={{ display: "flex", justifyContent: "flex-end" }}>Footer</div>}
onChange={setOpen}
>
<button type="button" onClick={() => setOpen((v) => !v)}>
Toggle
</button>
</Menu>
);
}Positions
Use `position` to place the panel around the anchor (top / bottom / left / right).
Live preview
position
import Menu from "@/_components/menu/Menu";
export default function Example() {
return (
<>
<Menu isOpen position="top" contentBody={<div />}><button>Top</button></Menu>
<Menu isOpen position="bottom" contentBody={<div />}><button>Bottom</button></Menu>
<Menu isOpen position="left" contentBody={<div />}><button>Left</button></Menu>
<Menu isOpen position="right" contentBody={<div />}><button>Right</button></Menu>
</>
);
}Hover trigger (external)
Hover is implemented outside of Menu (you still control `isOpen`).
Live preview
isOpen).Hover wrapper
import { useRef, useState } from "react";
import Menu from "@/packages/ui/src/_components/menu/Menu";
function HoverWrap(props: { setOpen: (v: boolean) => void; children: React.ReactNode }) {
const t = useRef<number | null>(null);
const openNow = () => {
if (t.current) window.clearTimeout(t.current);
t.current = null;
props.setOpen(true);
};
const closeSoon = () => {
if (t.current) window.clearTimeout(t.current);
t.current = window.setTimeout(() => props.setOpen(false), 180);
};
return (
<span onPointerEnter={openNow} onPointerLeave={closeSoon}>
{props.children}
</span>
);
}
export default function Example() {
const [open, setOpen] = useState(false);
return (
<HoverWrap setOpen={setOpen}>
<Menu isOpen={open} position="top" contentBody={<div />}>
<button>Hover me</button>
</Menu>
</HoverWrap>
);
}Menu_item states
Selection roles, disabled/active states, and the danger tone.
Live preview
Menu_item
import Menu_item from "@/_components/menu/menu_item/Menu_item";
export default function Example() {
return (
<>
<Menu_item label="Default" />
<Menu_item disabled label="Disabled" />
<Menu_item tone="danger" label="Delete" trailingText="⌫" />
<Menu_item role="menuitemcheckbox" selected label="Enabled" />
<Menu_item role="menuitemradio" selected label="Option A" />
</>
);
}Interactive playground
Quick way to validate type/style/variants/position combinations within the live view.
Live preview
Playground
import { useState } from "react";
import Menu from "@/packages/ui/src/_components/menu/Menu";
export default function Example() {
const [open, setOpen] = useState(false);
return (
<Menu
isOpen={open}
type="plain"
style="filled"
variants="neutral"
position="bottom"
contentBody={<div className="cssnt-menu__panel">...</div>}
onChange={setOpen}
>
<button type="button" onClick={() => setOpen((v) => !v)}>Toggle</button>
</Menu>
);
}On this page
Menu Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| isOpen | boolean | Yes | — | Controls visibility (`data-state="open" | "closed"`). The menu is always in the DOM; CSS handles show/hide. |
| position | "top" | "bottom" | "left" | "right" | No | "top" | Sets `data-side` to place the panel around the anchor (used by CSS positioning rules). |
| type | "plain" | "rich" | No | "plain" | Visual/content mode. `rich` enables the optional `text` header and `contentFooter` section. |
| style | "filled" | "outlined" | No | "filled" | Surface style preset. `outlined` adds the outline style class (bordered surface). |
| variants | "neutral" | "neutral-inverse" | "primary" | "secondary" | "tertiary" | "info" | "success" | "warning" | "danger" | No | "neutral" | Color/tone preset mapped to `cssnt-menu--*` classes. |
| title | string | No | undefined | Header text. Rendered only when `type="rich"`. |
| contentBody | React.ReactNode | string | No | undefined | Main panel content (commonly a list of `Menu_item`). |
| contentFooter | React.ReactNode | string | No | undefined | Footer slot. Rendered only when `type="rich"`. |
| children | React.ReactNode | Yes | — | Trigger/anchor content. You control how it opens (click, hover, etc). |
| onChange | (nextOpen: boolean) => void | No | undefined | Optional callback for open state changes. Treat the component as controlled: you still need to update `isOpen` in your state. |
Menu_item Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| label | React.ReactNode | Yes | — | Main label content (primary text). |
| supportingText | React.ReactNode | No | undefined | Secondary line under the label. |
| leading | React.ReactNode | No | undefined | Leading slot (icon/avatar). |
| trailing | React.ReactNode | No | undefined | Trailing slot (chevron/custom node). |
| badge | React.ReactNode | No | undefined | Optional badge rendered inside the trailing area. |
| trailingText | React.ReactNode | No | undefined | Optional trailing text (e.g., shortcut). |
| size | "sm" | "md" | "lg" | No | "md" | Size modifier class. `md` adds no extra size class. |
| tone | "default" | "danger" | No | "default" | Visual tone. `danger` applies a danger style (useful for destructive actions). |
| role | "menuitem" | "menuitemcheckbox" | "menuitemradio" | No | "menuitem" | ARIA role applied to the underlying button. |
| selected | boolean | No | false | Selection state (adds `is-selected`). Useful with checkbox/radio roles. |
| active | boolean | No | false | Active/pressed-like state (adds `is-active`). |
| disabled | boolean | No | false | Disables the button and sets `aria-disabled`. |
| onSelect | (e: React.MouseEvent<HTMLButtonElement>) => void | No | undefined | Called on click. Use it to perform an action and close the menu externally. |
| ariaLabel | string | No | undefined | Sets `aria-label` on the underlying button (useful for icon-only items). |
| className | string | No | undefined | Appended to the root button classes. |
| style | React.CSSProperties | No | undefined | Inline styles for the root button. |
| id | string | No | undefined | Sets the `id` on the underlying button. |