Menu

The menu component provides a context menu for navigating a list of options.

WAI-ARIA: Menu Pattern

Example

Sources

+page.svelte

<script lang="ts">
  import Menu from './Menu.svelte'
  import MenuItem from './MenuItem.svelte'
  import MenuDivider from './MenuDivider.svelte'
  import MenuSubmenu from './MenuSubmenu.svelte'
</script>

<Menu>
  <MenuItem href="#">New tab</MenuItem>
  <MenuItem href="#">New window</MenuItem>
  <MenuDivider />
  <MenuItem>
    History
    <MenuSubmenu slot="submenu">
      <MenuItem href="#">Show full history</MenuItem>
      <MenuItem href="#">History by site</MenuItem>
      <MenuItem href="#">Recently closed</MenuItem>
      <MenuItem href="#">Tabs from other devices</MenuItem>
    </MenuSubmenu>
  </MenuItem>
  <MenuItem href="#">Downloads</MenuItem>
  <MenuItem>
    Bookmarks
    <MenuSubmenu slot="submenu">
      <MenuItem href="#">
        Show all bookmarks
        <MenuSubmenu slot="submenu">
          <MenuItem href="#">Bookmarks bar</MenuItem>
          <MenuItem href="#">Other bookmarks</MenuItem>
        </MenuSubmenu>
      </MenuItem>
      <MenuItem href="#">Bookmark manager</MenuItem>
      <MenuItem href="#">Add bookmark</MenuItem>
    </MenuSubmenu>
  </MenuItem>
  <MenuDivider />
  <MenuItem href="#">Settings</MenuItem>
  <MenuItem href="#">Exit</MenuItem>
</Menu>

Menu.svelte

<script lang="ts">
  import { createMenu } from 'louisette'
  import { setContext } from 'svelte'

  const { menuAttrs, ...menuContext } = createMenu()

  setContext('menu', menuContext)
</script>

<ul
  {...$menuAttrs}
  class="flex w-max min-w-[12rem] flex-col gap-1 rounded bg-white p-2 shadow-lg dark:bg-neutral-900"
>
  <slot />
</ul>

MenuDivider.svelte

<li
  role="separator"
  class="my-1 border-t border-neutral-200 dark:border-neutral-800"
/>

MenuItem.svelte

<script lang="ts">
  import { createKey, type Menu } from 'louisette'
  import { getContext, setContext } from 'svelte'
  import { ChevronRight } from 'lucide-svelte'

  export let href: string = ''

  // Generate a random key for this menu item
  const key = createKey()

  // Check if this menu item has a submenu
  const hasSubmenu = $$slots.submenu !== undefined

  // Get the menu item attributes
  const { itemAttrs, triggerAttrs } = getContext<Menu>('menu')

  // Set the key of the submenu if this menu item has one
  setContext('submenu', hasSubmenu ? key : null)
</script>

<li class="relative">
  {#if hasSubmenu}
    <span
      {...$triggerAttrs(key)}
      class="flex items-center justify-between rounded-sm px-4 py-1 text-sm hover:bg-neutral-200 dark:hover:bg-neutral-700"
    >
      <slot />
      <ChevronRight class="ml-2 h-4 w-4" />
    </span>
    <slot name="submenu" />
  {:else}
    <a
      {...$itemAttrs(key)}
      {href}
      class="flex items-center rounded-sm px-4 py-1 text-sm hover:bg-neutral-200 dark:hover:bg-neutral-700"
    >
      <slot />
    </a>
  {/if}
</li>

MenuSubmenu.svelte

<script lang="ts">
  import type { Menu } from 'louisette'
  import { getContext } from 'svelte'

  const key: string = getContext('submenu')

  if (!key) {
    throw new Error('Submenu must be used inside a Menu item component')
  }

  const { submenuAttrs, activePath } = getContext<Menu>('menu')
</script>

{#if key}
  <div
    class="absolute left-full top-0 z-10 -mt-2 pl-2"
    class:hidden={!$activePath.includes(key)}
  >
    <ul
      {...$submenuAttrs(key)}
      class="flex w-max min-w-[12rem] flex-col gap-1 rounded bg-white p-2 shadow-lg dark:bg-neutral-900"
    >
      <slot />
    </ul>
  </div>
{/if}