Menu#
Component <neo-menu> is a keyboard-navigable dropdown menu. Author a trigger plus a flat list of <neo-menuitem> / <neo-submenu> / <hr> children; the host wraps them into a fly-out panel and owns hover, focus, ArrowRight / ArrowLeft submenu navigation, Esc, and a global shortcut binding per item:
The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.
Signals
Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.
<neo-menu>
A triggerless menu panel. Nest it directly inside a <neo-button> (the button becomes the dropdown trigger). The trigger can also be a context menu: nest it in a <neo-contextmenu> and it opens at the pointer instead. Its children are the rows; it wraps them into a self-contained fly-out and owns hover, focus, ArrowRight-to-enter / ArrowLeft-to-exit, and a global keyboard shortcut per item. From a button host: click or focus the button and press
Reference#
Attributes #
| Name | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Controls visibility. |
placement | string | "bottom-start" | Anchor position relative to the trigger; same values as <neo-popover>. |
mode | "auto" | "cascade" | "push" | "auto" | Submenu presentation. cascade flies out to the side (desktop). push replaces the parent panel with a back row (mobile). auto picks based on (hover: none) and (pointer: coarse), (max-width: 36rem). Resolved value is mirrored to data-mode-effective. |
screen-offset | string | none | Minimum margin between the panel and viewport edge; inherited by descendant <neo-submenu>s via the --neo-menu-screen-offset CSS cascade. |
clamp-placement | boolean | false | Opt out of the default strict placement. The menu panel re-anchors its position to keep itself inside the viewport when content would otherwise overflow, instead of staying glued to the configured side and shrinking max-width/max-height. Inherited by descendant <neo-submenu>s unless they set their own value. |
min-fit-height | CSS length | "content" | "content" | Minimum vertical slot the strict placement requires. If the slot at the configured side is shorter and the opposite side has more room, the menu auto-flips (e.g. bottom-start -> top-start). content resolves from the menu panel's scroll height on each positioning pass. Inherited by descendant <neo-submenu>s via the --neo-menu-min-fit-height CSS cascade. |
min-fit-width | CSS length | "content" | "content" | Same as min-fit-height but for the horizontal axis (left-*/right-*). content resolves from the panel's scroll width. Use 0 to disable the horizontal flip. |
Slots #
| Name | Description |
|---|---|
host | Parent <neo-button> or <neo-contextmenu>: the button becomes the trigger; the context menu opens the panel at the pointer. There is no trigger child. |
| default | Children (<neo-menuitem>, <neo-submenu>, or <hr> separators) are wrapped into the fly-out panel on connect. |
Events #
| Name | Detail | Bubbles | Description |
|---|---|---|---|
neo-menu-open | none | yes | Fires when the menu opens. |
neo-menu-close | none | yes | Fires when the menu closes. |
neo-menuitem-select | { value: string | null } | yes | Fires when any descendant <neo-menuitem> activates. The root menu auto-closes after. |
CSS variables #
| Name | Default | Description |
|---|---|---|
--neo-menu-bg | var(--btn-bg) | Panel background. |
--neo-menu-color | var(--page-fg) | Panel text color. |
--neo-menu-border-color | var(--btn-border) | Panel border color. |
--neo-menu-border-width | 1px | Panel border width. |
--neo-menu-radius | var(--page-radius, 0.625rem) | Panel corner radius. |
--neo-menu-padding | calc(var(--page-spacing, 0.25rem) * 1) | Padding around the panel rows. |
--neo-menu-shadow | 0 1px 2px rgba(0, 0, 0, 0.06), 0 8px 28px rgba(0, 0, 0, 0.18) | Panel box shadow. |
--neo-menu-min-width | 12rem | Panel minimum width. |
--neo-menu-z-index | 1200 | Panel stacking order. |
--neo-menu-item-radius | calc(var(--page-radius, 0.625rem) * 0.6) | Row corner radius. |
--neo-menu-item-padding-y | calc(var(--page-spacing, 0.25rem) * 1.5) | Vertical padding of a row. |
--neo-menu-item-padding-x | calc(var(--page-spacing, 0.25rem) * 2.5) | Horizontal padding of a row. |
--neo-menu-item-hover-bg | color-mix(in srgb, var(--page-fg) 8%, transparent) | Background of the focused row. |
--neo-menu-item-active-bg | color-mix(in srgb, var(--page-fg) 12%, transparent) | Background of an open submenu's trigger row. |
--neo-menu-shortcut-color | color-mix(in srgb, var(--page-fg) 55%, transparent) | Color of the trailing shortcut hint and submenu chevron. |
--neo-menu-separator-color | color-mix(in srgb, var(--page-fg) 12%, transparent) | Color of an <hr> separator. |
--neo-menu-separator-width | 1px | Thickness of an <hr> separator. |
<neo-menuitem>
A leaf row inside a menu or submenu. Activates on click, shortcut binding; emits neo-menuitem-select and the enclosing menu collapses the chain.
Attributes #
| Name | Type | Default | Description |
|---|---|---|---|
value | string | none | Identifier carried in the neo-menuitem-select event detail. |
disabled | boolean | false | Non-interactive state. Skips the focus ring, swallows clicks, and ignores the bound shortcut. |
shortcut | string | none | Global hotkey, e.g. Mod+S, Cmd+K, Shift+Alt+P. Mod resolves to ⌘ on macOS, Ctrl elsewhere. Renders a trailing kbd-styled hint and registers a document-level keydown that synthesizes activation. Suppressed when the user is typing into an <input>, <textarea>, or contentEditable. |
Slots #
| Name | Description |
|---|---|
| default | Item label. May contain text, icons, or any other markup. |
Events #
| Name | Detail | Bubbles | Description |
|---|---|---|---|
neo-menuitem-select | { value: string | null } | yes | Fires on click, Enter, Space, or shortcut activation. The enclosing menu listens and closes the chain. |
<neo-submenu>
A nested fly-out. Place it as a child of <neo-menu> or another <neo-submenu>; the host auto-renders the trigger row from label, and the element's children become the panel. Opens on hover, click, or
Attributes #
| Name | Type | Default | Description |
|---|---|---|---|
label | string | none | Required. Trigger row text. The host auto-renders the row + chevron. |
open | boolean | false | Controls fly-out visibility. |
placement | string | "right-start" | Anchor position relative to the trigger row. |
disabled | boolean | false | Disables the entire branch: the trigger row is unfocusable and hover/click no longer opens. |
screen-offset | string | none | Minimum margin between the panel and viewport edge; inherits from the ancestor <neo-menu> unless set directly. |
clamp-placement | boolean | false | Opt out of the default strict placement and re-anchor the submenu panel position to fit. The ancestor <neo-menu>'s value is also honored, so usually only the root needs to set it. |
min-fit-height | CSS length | "content" | "content" | Minimum vertical slot the strict placement requires; inherits from the ancestor <neo-menu> unless set directly. |
min-fit-width | CSS length | "content" | "content" | Same as min-fit-height but for the horizontal axis. |
Slots #
| Name | Description |
|---|---|
| default | Children (<neo-menuitem>, nested <neo-submenu>, or <hr> separators) are wrapped into the fly-out panel on connect. |
Events #
| Name | Detail | Bubbles | Description |
|---|---|---|---|
neo-submenu-open | none | yes | Fires when the fly-out opens. |
neo-submenu-close | none | yes | Fires when the fly-out closes. |
Examples#
Push mode#
Submenus replace the root panel rather than fly out to the side, with a synthesized back row. This is the native mobile pattern and the default for narrow viewports ((hover: none) and (pointer: coarse) or (max-width: 36rem)) when mode is left at its default auto. Forced here to preview on desktop.
Open above#
Anchor the panel at top-start. Strict placement is the default: the panel stays glued to top-*, shrinks its max-height to the slot above the trigger, and scrolls the items inside. Submenu fly-outs inherit. Toggle clamp-placement on to opt into the alternative: the menu re-anchors below the trigger when there isn't enough room above.
Disabled rows#
Disabled items skip the focus ring, swallow clicks, and ignore their bound shortcut. The submenu's own disabled attribute disables the entire branch.
Examples: Datastar#
Morphing during interaction#
A Datastar app can fat-morph the page at any moment, so the rows under a <neo-menu> may appear, disappear, or flip to disabled while the menu is open. Autoplay cycles the states below to simulate those server-driven morphs: open the menu and watch it reconcile in place: the trigger and panel stay mounted, the menu stays open, and focus survives.
The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.
Signals
Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.