Skip to content
omaris

Containment

Context Menu

The right-click menu.

import { ContextMenu } from 'omaris'
Learn

Examples

Basic

Right-click the card — or press and hold it on a touch screen.

Right-click, or press and hold

Last action: nothing yet

<script lang="ts">	import { ContextMenu, MenuItem, Divider } from 'omaris';​	let last = $state('nothing yet');</script>​<div class="flex flex-col items-center gap-3">	<ContextMenu label="File actions">		{#snippet trigger()}			<div				class="grid h-32 w-72 max-w-full place-items-center rounded-shape-md border border-dashed border-border bg-surface-container-low text-sm text-muted-foreground"			>				Right-click, or press and hold			</div>		{/snippet}		<MenuItem onclick={() => (last = 'Open')} shortcut="⏎">Open</MenuItem>		<MenuItem onclick={() => (last = 'Rename')} shortcut="F2">Rename</MenuItem>		<Divider />		<MenuItem tone="destructive" onclick={() => (last = 'Delete')}>Delete</MenuItem>	</ContextMenu>	<p class="text-sm text-muted-foreground">Last action: {last}</p></div>

Per row

One menu per row: each list item wraps its own, with its own actions.

report.pdf right-click
notes.md right-click
budget.xlsx right-click

Last action: nothing yet

<script lang="ts">	import { ContextMenu, MenuItem, MenuLabel, Divider } from 'omaris';​	const files = ['report.pdf', 'notes.md', 'budget.xlsx'];	let last = $state('nothing yet');</script>​<div class="flex flex-col items-center gap-3">	<div class="w-72 max-w-full overflow-hidden rounded-shape-md border border-border">		{#each files as file (file)}			<ContextMenu label="{file} actions">				{#snippet trigger()}					<div						class="flex items-center justify-between border-b border-border px-3 py-2.5 text-sm last:border-b-0 hover:bg-surface-container"					>						{file}						<span class="text-xs text-muted-foreground">right-click</span>					</div>				{/snippet}				<MenuLabel>{file}</MenuLabel>				<MenuItem onclick={() => (last = `Open ${file}`)}>Open</MenuItem>				<MenuItem onclick={() => (last = `Share ${file}`)}>Share</MenuItem>				<Divider />				<MenuItem tone="destructive" onclick={() => (last = `Delete ${file}`)}>Delete</MenuItem>			</ContextMenu>		{/each}	</div>	<p class="text-sm text-muted-foreground">Last action: {last}</p></div>

Overridden

closeOnSelect={false} keeps the menu open while a set of toggles is worked through, and the surface takes its own width.

Toggles that stay open
<script lang="ts">	import { ContextMenu, MenuItem, MenuLabel } from 'omaris';​	let pinned = $state(true);	let dense = $state(false);</script>​<ContextMenu label="View" closeOnSelect={false} classes={{ surface: 'min-w-64 rounded-shape-lg' }}>	{#snippet trigger()}		<div			class="grid h-32 w-72 max-w-full place-items-center rounded-shape-md border border-dashed border-border bg-surface-container-low text-sm text-muted-foreground"		>			Toggles that stay open		</div>	{/snippet}	<MenuLabel>View</MenuLabel>	<MenuItem selected={pinned} onclick={() => (pinned = !pinned)}>Pinned first</MenuItem>	<MenuItem selected={dense} onclick={() => (dense = !dense)}>Dense rows</MenuItem></ContextMenu>

When to use it

Use it for

  • The actions that belong to one thing: a file, a row, a card, a node in a Tree View. A visible button per row would clutter the list.
  • A long list of actions people run quickly: open, rename, duplicate, share, delete.
  • A power-user path in a dense surface like a board, a canvas or a table, next to the ordinary buttons, not instead of them.

Not for

  • The only way to reach an action. A context menu is invisible until guessed at, so every row must also be reachable somewhere obvious: a Menu off a ⋯ button, a toolbar, a shortcut.
  • A menu opened by a button → Menu.
  • Choosing a value → Select or Combobox. This menu is for actions.
  • The whole page. A menu on <body> replaces the browser's own, which people use to open a link in a new tab.

Do

  • Repeat the important rows somewhere visible. If the only way to delete is a right-click, the feature does not exist on a phone.
  • Keep it to about seven rows, grouped with Divider and named with MenuLabel when the target is not obvious. Put the destructive row last, with tone="destructive", behind a divider.
  • Wrap the smallest region that owns the actions: one row, not the list.
  • Leave longPress on. It is the same gesture on a touch screen; off makes the menu desktop-only.
  • Set closeOnSelect={false} when the rows are toggles.

Don't

  • Nest submenus. A shortcut with a second level is not a shortcut.
  • Use it on a link or a text selection. People want the browser's own menu there.
  • Open it on left click. That is a Menu.
  • Add a row that needs a second click somewhere else to make sense.

API

ContextMenu

The right-click menu.

Wraps a region and opens a Menu at the pointer when it is right-clicked — or long-pressed, which is the same gesture on a touch screen and the reason a context menu is not a desktop-only idea. The rows are the same MenuItem, MenuLabel and Divider a Menu takes, and choosing one closes the menu.

It is always a shortcut, never the only way to do something: every action in here must also be reachable from a button, a menu or the keyboard.

import { ContextMenu } from 'omaris'
<ContextMenu>  {#snippet trigger()}    <Card>Right-click me</Card>  {/snippet}  <MenuItem onclick={rename}>Rename</MenuItem>  <MenuItem tone="destructive" onclick={remove}>Delete</MenuItem></ContextMenu>

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

surface
No description in the source yet.
trigger
The wrapper around whatever opens the menu — w-full, usually.

Props

open bindable

Defaults to false

boolean

Bindable.

size

Defaults to 'md'

MenuSize
disabled

Defaults to false

boolean

Never opens, and the browser's own menu comes back.

longPress

Defaults to 500

number

How long a touch has to be held before the menu opens, in ms. 0 turns the gesture off and leaves right-click as the only way in.

closeOnSelect

Defaults to true

boolean

Leave the menu open after a row is chosen — for a set of toggles.

label
string

Accessible name for the menu.

mobileSheet

Defaults to true

boolean

On a phone, rise from the bottom edge as a sheet — dragged down to dismiss, rows a thumb can hit — rather than opening under a finger that is covering it. false keeps it at the press point.

trigger
Snippet

The region that answers the right-click.

children
Snippet

The rows — MenuItem, MenuLabel, Divider.

onopen
(position: { x: number; y: number }) => void

Fires with the coordinates the menu opened at.

class
string
classes
MenuClasses

Per-part Tailwind overrides. class covers the wrapper.