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 update 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 to 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 does not open it. |
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. 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, where 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. The rows under a <neo-menu> may then 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 update the preview, applied live while Sync is on, or when you click Patch.