ShardsUI is in beta. APIs may change before 1.0.

Context Menu

A right-click menu.

Right click here
<script lang="ts">
  import { ContextMenu } from '@shardsui/svelte/context-menu'
</script>

<ContextMenu.Root>
  <ContextMenu.Trigger
    class="flex h-48 w-60 items-center justify-center rounded-md border border-dashed border-gray-300 text-sm font-normal text-gray-600 select-none"
  >
    Right click here
  </ContextMenu.Trigger>
  <ContextMenu.Portal>
    <ContextMenu.Positioner class="outline-hidden">
      <ContextMenu.Popup
        class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-opacity duration-100 ease-out data-ending-style:opacity-0"
      >
        <ContextMenu.Item
          class="flex py-2 pr-8 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
          >Open</ContextMenu.Item
        >
        <ContextMenu.Item
          class="flex py-2 pr-8 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
          >Rename</ContextMenu.Item
        >
        <ContextMenu.Separator class="m-1 h-px bg-gray-200" />
        <ContextMenu.Item
          class="flex py-2 pr-8 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
          >Delete</ContextMenu.Item
        >
      </ContextMenu.Popup>
    </ContextMenu.Positioner>
  </ContextMenu.Portal>
</ContextMenu.Root>

Anatomy

<script>
  import { ContextMenu } from '@shardsui/svelte/context-menu'
</script>

<ContextMenu.Root>
  <ContextMenu.Trigger />
  <ContextMenu.Portal>
    <ContextMenu.Backdrop />
    <ContextMenu.Positioner>
      <ContextMenu.Popup>
        <ContextMenu.Arrow />
        <ContextMenu.Item />
        <ContextMenu.LinkItem />
        <ContextMenu.Separator />
        <ContextMenu.SubmenuRoot>
          <ContextMenu.SubmenuTrigger />
        </ContextMenu.SubmenuRoot>
        <ContextMenu.Group>
          <ContextMenu.GroupLabel />
        </ContextMenu.Group>
        <ContextMenu.RadioGroup>
          <ContextMenu.RadioItem>
            <ContextMenu.RadioItemIndicator />
          </ContextMenu.RadioItem>
        </ContextMenu.RadioGroup>
        <ContextMenu.CheckboxItem>
          <ContextMenu.CheckboxItemIndicator />
        </ContextMenu.CheckboxItem>
      </ContextMenu.Popup>
    </ContextMenu.Positioner>
  </ContextMenu.Portal>
</ContextMenu.Root>

Most parts (Backdrop, Popup, Item, etc.) are re-exported from the Menu component, so they share the same props and data attributes. Positioner is the exception: its align, offset, arrowPadding and positionMethod defaults follow the pointer instead of a trigger element, and are documented below.

Usage guidelines

  • Use context menus as an enhancement: they may be undiscoverable on touch devices or with assistive technology, so always pair them with a visible control for the same actions.

Examples

The Menu page has more demos — most of its patterns carry over to the context menu.

Using with Menu

A context menu should supplement a primary way to perform the same actions. This image card exposes actions through a visible menu button and reuses them in the context menu for right-click and long-press users.

Station Hofplein

JPG, 2.4 MB

<script lang="ts">
  import { ContextMenu } from '@shardsui/svelte/context-menu'
  import { Menu } from '@shardsui/svelte/menu'

  const actions = ['Preview', 'Download', 'Copy link', 'Rename']
</script>

<div
  class="group relative w-64 overflow-hidden rounded-md bg-gray-50 text-left text-gray-900 outline-1 outline-gray-200 select-none"
>
  <ContextMenu.Root>
    <ContextMenu.Trigger>
      <div class="h-32 w-full bg-linear-to-br from-gray-200 to-gray-400"></div>
      <div class="p-2">
        <p class="text-sm/5">Station Hofplein</p>
        <p class="text-xs/4 text-gray-500">JPG, 2.4 MB</p>
      </div>
    </ContextMenu.Trigger>
    <ContextMenu.Portal>
      <ContextMenu.Positioner class="outline-hidden">
        <ContextMenu.Popup
          class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
        >
          {@render actionItems(ContextMenu)}
        </ContextMenu.Popup>
      </ContextMenu.Positioner>
    </ContextMenu.Portal>
  </ContextMenu.Root>

  <Menu.Root>
    <Menu.Trigger
      aria-label="Image actions"
      class="absolute top-2 right-2 flex size-8 items-center justify-center rounded-md bg-gray-50 text-gray-900 opacity-0 outline-1 outline-gray-200 select-none group-hover:opacity-100 hover:bg-gray-100 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-popup-open:bg-gray-100 data-popup-open:opacity-100 any-pointer-coarse:opacity-100"
    >
      {@render moreVerticalIcon()}
    </Menu.Trigger>
    <Menu.Portal>
      <Menu.Positioner class="outline-hidden" align="end" sideOffset={8}>
        <Menu.Popup
          class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
        >
          {@render actionItems(Menu)}
        </Menu.Popup>
      </Menu.Positioner>
    </Menu.Portal>
  </Menu.Root>
</div>

{#snippet actionItems(parts: typeof Menu | typeof ContextMenu)}
  {#each actions as action (action)}
    <parts.Item
      class="flex py-2 pr-8 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
      >{action}</parts.Item
    >
  {/each}
  <parts.Separator class="m-1 h-px bg-gray-200" />
  <parts.Item
    class="flex py-2 pr-8 pl-4 text-sm/4 text-red-700 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
    >Delete</parts.Item
  >
{/snippet}

{#snippet moreVerticalIcon()}
  <svg class="size-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
    <path
      d="M9.5 13c0 .8284-.67157 1.5-1.5 1.5s-1.5-.6716-1.5-1.5.67157-1.5 1.5-1.5 1.5.6716 1.5 1.5m0-5c0 .82843-.67157 1.5-1.5 1.5S6.5 8.82843 6.5 8 7.17157 6.5 8 6.5s1.5.67157 1.5 1.5m0-5c0 .82843-.67157 1.5-1.5 1.5S6.5 3.82843 6.5 3 7.17157 1.5 8 1.5s1.5.67157 1.5 1.5"
    />
  </svg>
{/snippet}

Nested menu

Nest <ContextMenu.SubmenuRoot> inside the popup to create a submenu, and use <ContextMenu.SubmenuTrigger> for the menu item that opens the nested menu.

Right click here
<script lang="ts">
  import { ContextMenu } from '@shardsui/svelte/context-menu'

  const folders = ['Design', 'Engineering', 'Marketing']

  function submenuOffset({ side }: { side: string }) {
    return side === 'top' || side === 'bottom' ? 4 : -4
  }
</script>

<ContextMenu.Root>
  <ContextMenu.Trigger
    class="flex h-48 w-60 items-center justify-center rounded-md border border-dashed border-gray-300 text-sm font-normal text-gray-600 select-none"
  >
    Right click here
  </ContextMenu.Trigger>
  <ContextMenu.Portal>
    <ContextMenu.Positioner class="outline-hidden">
      <ContextMenu.Popup
        class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-opacity duration-100 ease-out data-ending-style:opacity-0"
      >
        <ContextMenu.Item
          class="flex py-2 pr-6 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
          >Open</ContextMenu.Item
        >

        <ContextMenu.SubmenuRoot>
          <ContextMenu.SubmenuTrigger
            class="flex items-center justify-between gap-4 py-2 pr-2 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900 data-popup-open:relative data-popup-open:z-0 data-popup-open:before:absolute data-popup-open:before:inset-x-1 data-popup-open:before:inset-y-0 data-popup-open:before:z-[-1] data-popup-open:before:rounded-xs data-popup-open:before:bg-gray-100 data-highlighted:data-popup-open:before:bg-gray-900"
          >
            Move to folder
            {@render chevronRightIcon()}
          </ContextMenu.SubmenuTrigger>
          <ContextMenu.Portal>
            <ContextMenu.Positioner
              class="outline-hidden"
              alignOffset={submenuOffset}
              sideOffset={submenuOffset}
            >
              <ContextMenu.Popup
                class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
              >
                {#each folders as folder (folder)}
                  <ContextMenu.Item
                    class="flex py-2 pr-6 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
                    >{folder}</ContextMenu.Item
                  >
                {/each}
              </ContextMenu.Popup>
            </ContextMenu.Positioner>
          </ContextMenu.Portal>
        </ContextMenu.SubmenuRoot>

        <ContextMenu.Separator class="m-1 h-px bg-gray-200" />

        <ContextMenu.Item
          class="flex py-2 pr-6 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
          >Delete</ContextMenu.Item
        >
      </ContextMenu.Popup>
    </ContextMenu.Positioner>
  </ContextMenu.Portal>
</ContextMenu.Root>

{#snippet chevronRightIcon()}
  <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
    <path
      d="M9.5 18.25L15.75 12L9.5 5.75"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

API reference

Root

Opens on right click or long press. Doesn't render its own HTML element.

PropTypeDefault

Trigger

An area that opens the menu on right click or long press. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-popup-openPresent when the context menu is open.
data-pressedPresent when the menu is open.

Positioner

Positions the context menu popup at the press point. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-anchor-hiddenPresent when the anchor is hidden.
data-instantPresent when animations should be instant.
data-nestedPresent when the menu is a submenu.
CSS VariableDescription
--available-widthAvailable width between the anchor and the viewport edge.
--available-heightAvailable height between the anchor and the viewport edge.
--anchor-widthWidth of the press point (0, or 10 after a long press).
--anchor-heightHeight of the press point (0, or 10 after a long press).
--transform-originTransform origin for scale animations.

Other parts

Portal, Backdrop, Popup, Arrow, Item, LinkItem, Group, GroupLabel, RadioGroup, RadioItem, RadioItemIndicator, CheckboxItem, CheckboxItemIndicator, SubmenuRoot, SubmenuTrigger, Separator — see Menu for their props and data attributes.