Skip to content

Navigation

Context Menu

A right-click, long-press and Shift+F10 menu that reuses the DropdownMenu items.

<script>
  import { ContextMenu, DropdownMenuItem, DropdownMenuSeparator } from "@codeatlasdev/ui";
</script>

<ContextMenu role="group" aria-label="Project file">
  quarterly-report.pdf
  {#snippet menu()}
    <DropdownMenuItem onclick={rename}>Rename</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem variant="destructive" onclick={remove}>Delete</DropdownMenuItem>
  {/snippet}
</ContextMenu>

ContextMenu wraps a region and opens a DropdownMenu at the pointer. The menu snippet takes the same items: DropdownMenuItem, checkbox and radio items, DropdownMenuSub, labels and separators, with the same keyboard support (arrows, Home, End, typeahead, Enter or Space, Escape, ArrowRight and ArrowLeft for submenus).

Three triggers open it. A right-click opens it at the pointer and cancels the browser’s own menu; a second right-click moves it. A touch that stays put for longPressDelay (500 ms) opens it at the finger; lifting early or dragging more than 10 px cancels the press, and the browser’s own long-press menu is cancelled. Shift+F10 or the ContextMenu key on the focused region opens it at the region’s start, on the first item. Escape closes it and focus returns to the region.

The region is focusable by default (tabindex="0") so the keyboard trigger is reachable; set focusable={false} when the region already contains its own focusable target (a row with a link) and put the handlers’ role there. Give the region a role and an aria-label through the rest props when it is not plain content. A context menu is never the only way to reach an action: keep the same actions on a visible button, for example a DropdownMenu trigger.

Text fields (input, textarea, select, editable content) keep the browser’s menu (copy, paste, spellcheck); disabled keeps it everywhere. bind:open and onOpenChange work as on DropdownMenu.

#942

File menu

Right-click, long-press or Shift+F10. Checkbox items keep the menu open, Move to opens a submenu, and Delete is destructive.

quarterly-report.pdf. Right-click, long-press, or focus it and press Shift+F10.
Pinned: no

Reference #942 · Pick in the gallery

Code
svelte
<script lang="ts">
  import {
    announce,
    ContextMenu,
    DropdownMenuCheckboxItem,
    DropdownMenuItem,
    DropdownMenuSeparator,
    DropdownMenuSub,
    DropdownMenuSubTrigger,
  } from '@codeatlasdev/ui';

  let last = $state('');
  let pinned = $state(false);

  function pick(action: string) {
    last = action;
    announce(`Last action: ${action}`);
  }
</script>

<div class="cm-file">
<ContextMenu class="cm-area" role="group" aria-label="Project file">
  <span>quarterly-report.pdf. Right-click, long-press, or focus it and press Shift+F10.</span>
  {#snippet menu()}
    <DropdownMenuItem onclick={() => pick('Rename')}>Rename</DropdownMenuItem>
    <DropdownMenuCheckboxItem bind:checked={pinned}>Pinned</DropdownMenuCheckboxItem>
    <DropdownMenuSub>
      {#snippet trigger()}<DropdownMenuSubTrigger>Move to</DropdownMenuSubTrigger>{/snippet}
      <DropdownMenuItem onclick={() => pick('Move to Archive')}>Archive</DropdownMenuItem>
      <DropdownMenuItem onclick={() => pick('Move to Shared')}>Shared</DropdownMenuItem>
    </DropdownMenuSub>
    <DropdownMenuSeparator />
    <DropdownMenuItem variant="destructive" onclick={() => pick('Delete')}>Delete</DropdownMenuItem>
  {/snippet}
</ContextMenu>
</div>

<span class="note">Pinned: {pinned ? 'yes' : 'no'}{last ? `. Last action: ${last}` : ''}</span>

<style>
  /* Scoped under this demo's wrapper: a bare global `.area` would restyle every other demo's `.area`. */
  .cm-file :global(.cm-area) {
    display: grid;
    place-items: center;
    min-block-size: 8rem;
    padding: var(--ca-space-4);
    border: var(--ca-border-width) dashed var(--ca-outline-subtle);
    border-radius: var(--ca-radius-md);
    color: var(--ca-text-secondary);
    font-size: var(--ca-font-size-body2);
    text-align: center;
  }
  .note { font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>
#943

One menu per row

Each row is its own region. Tab to a row and press Shift+F10, or long-press on touch.

  • Invoice 1042
  • Invoice 1043
  • Invoice 1044
Tab to a row and press Shift+F10, or long-press on touch.

Reference #943 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { ContextMenu, DropdownMenuItem, DropdownMenuLabel } from '@codeatlasdev/ui';

  const rows = ['Invoice 1042', 'Invoice 1043', 'Invoice 1044'];
  let last = $state('');
</script>

<ul class="cm-rows">
  {#each rows as row (row)}
    <li>
      <ContextMenu class="cm-row" role="group" aria-label={row} longPressDelay={400}>
        <span>{row}</span>
        {#snippet menu()}
          <DropdownMenuLabel>{row}</DropdownMenuLabel>
          <DropdownMenuItem onclick={() => (last = `Open ${row}`)}>Open</DropdownMenuItem>
          <DropdownMenuItem onclick={() => (last = `Send ${row}`)}>Send reminder</DropdownMenuItem>
        {/snippet}
      </ContextMenu>
    </li>
  {/each}
</ul>
<span class="note">{last ? `Last action: ${last}` : 'Tab to a row and press Shift+F10, or long-press on touch.'}</span>

<style>
  .cm-rows { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--ca-space-1); }
  /* The class goes through ContextMenu's `class` prop onto its own element, so it is reached with
     :global, but only under this demo's list: a bare global `.row` would restyle every other demo's `.row`. */
  .cm-rows :global(.cm-row) {
    padding: var(--ca-space-2) var(--ca-space-3);
    border-radius: var(--ca-radius-sm);
    font-size: var(--ca-font-size-body2);
    color: var(--ca-text-primary);
  }
  .cm-rows :global(.cm-row:hover), .cm-rows :global(.cm-row[data-state='open']) { background: var(--ca-action-hover); }
  .note { font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>

Props

PropTypeDefaultDescription
childrenSnippet–The region content. Right-click, long-press and Shift+F10 inside it open the menu.
menuSnippet–The menu content: DropdownMenu items, checkbox and radio items, submenus, labels and separators.
openbooleanfalseBindable. Whether the menu is open.
onOpenChange(open: boolean) => void–Called when the user opens or closes the menu, not when the bound value is set from code.
disabledbooleanfalseKeep the browser's own menu and open nothing.
longPressDelaynumber500Milliseconds a touch must stay down before the menu opens.
focusablebooleantrueMake the region reachable with Tab so Shift+F10 and the ContextMenu key work.
menuClassstring''Extra CSS classes on the menu surface.
classstring''Extra CSS classes on the region.