SvMenuList

The recursive menu surface behind SvMenu and SvContextMenu: a WAI-ARIA menu with roving focus, submenus, separators, icons, and shortcuts.

SvMenuList renders a MenuItem[] array as an accessible menu and self-imports to draw submenu flyouts. It owns all the keyboard behaviour - arrows, Home/End, Enter/Space, Escape, and ArrowRight/Left for submenus. Most apps use it indirectly through SvMenu or SvContextMenu, which wrap it in a portalled, positioned panel; reach for it directly only when you are building your own positioned surface.

Basic usage

<script lang="ts">
  import { SvMenuList, type MenuItem } from '@svgrid/grid'
  const items: MenuItem[] = [
    { label: 'Rename', onSelect: () => rename() },
    { separator: true },
    { label: 'Delete', onSelect: () => remove() },
  ]
</script>

<!-- Inside your own portalled, positioned panel -->
<SvMenuList {items} onclose={() => (open = false)} onselect={(i) => run(i)} />

Props

Prop Type Default Description
items ReadonlyArray<MenuItem> - The menu contents (see MenuItem below).
onclose () => void - Close the whole menu (a leaf was chosen or Escape at root).
onselect (item: MenuItem) => void - Report the chosen leaf item.
submenu boolean false Marks a nested list, so ArrowLeft closes back to the parent.
Field Type Description
label string Item text. Omit and set separator for a divider.
separator boolean Render a divider instead of an item.
icon Snippet Leading icon.
shortcut string Right-aligned hint, e.g. a keyboard shortcut.
disabled boolean Dim and skip the item during roving focus.
children MenuItem[] Nested submenu items (renders a flyout).
onSelect () => void Per-item callback fired when the item is chosen.

Patterns

Prefer the wrappers

For a trigger-opened dropdown or a right-click menu, use SvMenu or SvContextMenu - they handle positioning, portalling, and dismissal so you only supply items and onSelect.

Custom surfaces

When you need a menu somewhere the wrappers do not fit - inside a toolbar flyout, say - render SvMenuList in your own container and wire the two callbacks:

{#if open}
  <div class="my-panel" style:position="fixed" style:top="{y}px" style:left="{x}px">
    <SvMenuList {items} onclose={() => (open = false)} onselect={(i) => run(i)} />
  </div>
{/if}

Item vs list callbacks

Each MenuItem can carry its own onSelect; the list-level onselect fires in addition, reporting the same leaf item for centralized handling.

Inside a popover

The wrappers cover most cases, but you can drop the surface into any positioned container yourself - here a SvPopover supplies the anchoring and dismissal while SvMenuList supplies the keyboard menu. Close the popover from onclose:

<script lang="ts">
  import { SvPopover, SvButton, SvMenuList, type MenuItem } from '@svgrid/grid'
  let open = $state(false)
  const items: MenuItem[] = [
    { label: 'Rename', onSelect: () => rename() },
    { label: 'Move to', children: [
      { label: 'Inbox', onSelect: () => move('inbox') },
      { label: 'Archive', onSelect: () => move('archive') },
    ]},
    { separator: true },
    { label: 'Delete', onSelect: () => remove() },
  ]
</script>

<SvPopover bind:open trigger="click" ariaLabel="File actions">
  {#snippet anchor()}<SvButton>File</SvButton>{/snippet}
  <SvMenuList {items} onclose={() => (open = false)}
    onselect={(i) => console.log(i.label)} />
</SvPopover>

Tip: leave submenu at its false default on the outermost list - the component sets it on the flyouts it renders for children itself, so ArrowLeft steps back to the parent only from within a nested list.

Accessibility

See also