Menu

Non-modal menu surface with role="menu", open state, and overlay dismiss (Escape / outside press). Pair with MenuItem (role="menuitem") for item semantics.

PreviewMenu surface

Usage

tsx
import { useState } from "react";
import { Menu, MenuItem } from "@sometic/react/overlay";

export function Example() {
    const [open, setOpen] = useState(false);
    return (
        <>
            <button type="button" onClick={() => setOpen(true)}>
                Actions
            </button>
            <Menu open={open} onOpenChange={setOpen}>
                <MenuItem>Edit</MenuItem>
                <MenuItem>Duplicate</MenuItem>
                <MenuItem disabled>Delete</MenuItem>
            </Menu>
        </>
    );
}
tsx
import { useState } from "react";
import { Menu, MenuItem } from "@sometic/react/overlay";

export function Example(): JSX.Element {
    const [open, setOpen] = useState(false);
    return (
        <>
            <button type="button" onClick={() => setOpen(true)}>
                Actions
            </button>
            <Menu open={open} onOpenChange={setOpen}>
                <MenuItem>Edit</MenuItem>
                <MenuItem>Duplicate</MenuItem>
                <MenuItem disabled>Delete</MenuItem>
            </Menu>
        </>
    );
}
js
import { createMenuController, resolveMenu, resolveMenuItem } from "@sometic/dom/menu";

const panel = document.querySelector("#menu");
const controller = createMenuController({
    defaultOpen: false,
    getContent: () => panel,
    onOpenChange: (open) => {
        const view = resolveMenu({ open });
        Object.assign(panel.dataset, { state: view.attributes["data-state"] });
        panel.hidden = !open;
    },
});

document.querySelector("#open-menu").addEventListener("click", () => {
    controller.setOpen(true);
});

for (const item of panel.querySelectorAll("[data-menuitem]")) {
    const view = resolveMenuItem({
        disabled: item.hasAttribute("disabled"),
    });
    Object.assign(item, { role: "menuitem" });
    for (const [key, value] of Object.entries(view.attributes)) {
        item.setAttribute(key, value);
    }
}

Custom element not shipped in this beta; use the DOM controller.

How it works

  1. Resolve (resolveMenu / resolveMenuItem): menu and menuitem ARIA plus data-state / disabled attrs.
  2. Controller (createMenuController): non-modal overlay with optional positioning update via updatePosition.
  3. React: Menu owns the controller; MenuItem is resolve-only.

Anatomy

PartRole
Menu rootrole="menu"
Menu itemrole="menuitem"

Props / attributes

React MenuProps

Extends HTMLAttributes<HTMLDivElement>. Remaining native div attrs are forwarded to the panel when open (ref supported via the panel element).

PropTypeDefaultDescription
openbooleanControlled open
defaultOpenbooleanfalseUncontrolled initial
onOpenChange(open: boolean) => voidOpen changes
childrenReactNodeUsually MenuItem nodes
Native attrsremaining div HTML attrsForwarded to the panel

React MenuItemProps

Extends HTMLAttributes<HTMLDivElement>. Remaining native div attrs are forwarded to the item root.

PropTypeDefaultDescription
disabledbooleanfalseSets aria-disabled
checkedbooleanfalseOptional checked presentation
childrenReactNodeItem label
Native attrsremaining div HTML attrsForwarded to the menuitem root

Vue

No Vue Menu / MenuItem component adapter. Use React or the DOM controller. @sometic/vue does not re-export a Menu surface; call @sometic/dom/menu from a Vue setup if needed.

Custom element

CE not shipped. Use Vanilla DOM controller above (or React).

Events / callbacks

SurfaceEventPayload
ReactonOpenChangeboolean
Vue
Custom element
DOM controlleronOpenChangeboolean

Controlled vs uncontrolled

  • Controlled: pass open and update from onOpenChange (including Escape / outside press).
  • Uncontrolled: omit open, use defaultOpen; dismiss still notifies onOpenChange.

Accessibility

  • Use real menu/menuitem roles from resolve.
  • Keep a clear trigger with aria-haspopup="menu" in your opener when composing.
  • Prefer keyboard handling in your app layer for arrow keys in this beta (roving focus helpers can wrap the list).

Styling

Target [role="menu"], [role="menuitem"], [data-disabled], [data-state].

When to use / When not

Use for action menus and command lists.

Do not use for modal confirmations (Dialog) or pointer-positioned context menus (Context menu).

FAQ

Modal? No. Outside press and Escape dismiss.

Positioning? Call updatePosition on the DOM controller, or style the panel yourself in React.

Is there an sometic-menu custom element? No. CE not shipped — use @sometic/dom/menu or React.

Vue component? Not shipped. React + DOM are primary.

Does React forward native attrs? Yes, remaining HTMLAttributes<HTMLDivElement> merge onto the panel / item.

SSR? Create controllers only in the browser.