Epicenter
Local-first, open source apps
Right click here Long press here
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
let showBookmarks = $state(true);
let showFullURLs = $state(false);
let value = $state("pedro");
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenu.Trigger>
<ContextMenu.Content class="w-48">
<ContextMenu.Group>
<ContextMenu.Item>
Back
<ContextMenu.Shortcut>⌘[</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item disabled>
Forward
<ContextMenu.Shortcut>⌘]</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>
Reload
<ContextMenu.Shortcut>⌘R</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Sub>
<ContextMenu.SubTrigger>More Tools</ContextMenu.SubTrigger>
<ContextMenu.SubContent class="w-44">
<ContextMenu.Group>
<ContextMenu.Item>Save Page...</ContextMenu.Item>
<ContextMenu.Item>Create Shortcut...</ContextMenu.Item>
<ContextMenu.Item>Name Window...</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Item>Developer Tools</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Item variant="destructive">Delete</ContextMenu.Item>
</ContextMenu.Group>
</ContextMenu.SubContent>
</ContextMenu.Sub>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.CheckboxItem bind:checked={showBookmarks}
>Show Bookmarks</ContextMenu.CheckboxItem
>
<ContextMenu.CheckboxItem bind:checked={showFullURLs}
>Show Full URLs</ContextMenu.CheckboxItem
>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.RadioGroup bind:value>
<ContextMenu.Label>People</ContextMenu.Label>
<ContextMenu.RadioItem value="pedro">Pedro Duarte</ContextMenu.RadioItem
>
<ContextMenu.RadioItem value="colm">Colm Tuite</ContextMenu.RadioItem>
</ContextMenu.RadioGroup>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Root> Installation #
pnpm dlx shadcn-svelte@latest add context-menunpx shadcn-svelte@latest add context-menunpx shadcn-svelte@latest add context-menubun x shadcn-svelte@latest add context-menuInstall bits-ui:
pnpm i bits-ui -Dnpm i bits-ui -Dyarn install bits-ui -Dbun install bits-ui -DCopy and paste the following code into your project.
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import CheckIcon from '@lucide/svelte/icons/check';
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
inset,
children: childrenProp,
closeOnSelect = false,
...restProps
}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {
inset?: boolean;
children?: Snippet;
} = $props();
</script>
<ContextMenuPrimitive.CheckboxItem
bind:ref
{closeOnSelect}
bind:checked
bind:indeterminate
data-slot="context-menu-checkbox-item"
data-inset={inset}
class={cn(
"focus:bg-accent focus:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm data-inset:pl-8 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="absolute right-2 pointer-events-none">
{#if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</ContextMenuPrimitive.CheckboxItem>
Usage #
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
</script> <ContextMenu.Root>
<ContextMenu.Trigger>Right click here</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item>Profile</ContextMenu.Item>
<ContextMenu.Item>Billing</ContextMenu.Item>
<ContextMenu.Item>Team</ContextMenu.Item>
<ContextMenu.Item>Subscription</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root> Basic #
A simple context menu with a few actions.
Right click here Long press here
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Group>
<ContextMenu.Item>Back</ContextMenu.Item>
<ContextMenu.Item disabled>Forward</ContextMenu.Item>
<ContextMenu.Item>Reload</ContextMenu.Item>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Root> Submenu #
Use ContextMenu.Sub to nest secondary actions.
Right click here Long press here
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Group>
<ContextMenu.Item>
Copy
<ContextMenu.Shortcut>⌘C</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>
Cut
<ContextMenu.Shortcut>⌘X</ContextMenu.Shortcut>
</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Sub>
<ContextMenu.SubTrigger>More Tools</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
<ContextMenu.Group>
<ContextMenu.Item>Save Page...</ContextMenu.Item>
<ContextMenu.Item>Create Shortcut...</ContextMenu.Item>
<ContextMenu.Item>Name Window...</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Item>Developer Tools</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Item variant="destructive">Delete</ContextMenu.Item>
</ContextMenu.Group>
</ContextMenu.SubContent>
</ContextMenu.Sub>
</ContextMenu.Content>
</ContextMenu.Root> Shortcuts #
Add ContextMenu.Shortcut to show keyboard hints.
Right click here Long press here
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Group>
<ContextMenu.Item>
Back
<ContextMenu.Shortcut>⌘[</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item disabled>
Forward
<ContextMenu.Shortcut>⌘]</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>
Reload
<ContextMenu.Shortcut>⌘R</ContextMenu.Shortcut>
</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Item>
Save
<ContextMenu.Shortcut>⌘S</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>
Save As...
<ContextMenu.Shortcut>⇧⌘S</ContextMenu.Shortcut>
</ContextMenu.Item>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Root> Groups #
Group related actions and separate them with dividers.
Right click here Long press here
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Group>
<ContextMenu.Label>File</ContextMenu.Label>
<ContextMenu.Item>
New File
<ContextMenu.Shortcut>⌘N</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>
Open File
<ContextMenu.Shortcut>⌘O</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>
Save
<ContextMenu.Shortcut>⌘S</ContextMenu.Shortcut>
</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Label>Edit</ContextMenu.Label>
<ContextMenu.Item>
Undo
<ContextMenu.Shortcut>⌘Z</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>
Redo
<ContextMenu.Shortcut>⇧⌘Z</ContextMenu.Shortcut>
</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Item>
Cut
<ContextMenu.Shortcut>⌘X</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>
Copy
<ContextMenu.Shortcut>⌘C</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>
Paste
<ContextMenu.Shortcut>⌘V</ContextMenu.Shortcut>
</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Item variant="destructive">
Delete
<ContextMenu.Shortcut>⌫</ContextMenu.Shortcut>
</ContextMenu.Item>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Root> Icons #
Combine icons with labels for quick scanning.
Right click here Long press here
<script lang="ts">
import ClipboardPasteIcon from "@lucide/svelte/icons/clipboard-paste";
import CopyIcon from "@lucide/svelte/icons/copy";
import ScissorsIcon from "@lucide/svelte/icons/scissors";
import TrashIcon from "@lucide/svelte/icons/trash";
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Group>
<ContextMenu.Item>
<CopyIcon />
Copy
</ContextMenu.Item>
<ContextMenu.Item>
<ScissorsIcon />
Cut
</ContextMenu.Item>
<ContextMenu.Item>
<ClipboardPasteIcon />
Paste
</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Item variant="destructive">
<TrashIcon />
Delete
</ContextMenu.Item>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Root> Checkboxes #
Use ContextMenu.CheckboxItem for toggles.
Right click here Long press here
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
let showBookmarksBar = $state(true);
let showFullURLs = $state(false);
let showDeveloperTools = $state(true);
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Group>
<ContextMenu.CheckboxItem bind:checked={showBookmarksBar}>
Show Bookmarks Bar
</ContextMenu.CheckboxItem>
<ContextMenu.CheckboxItem bind:checked={showFullURLs}
>Show Full URLs</ContextMenu.CheckboxItem
>
<ContextMenu.CheckboxItem bind:checked={showDeveloperTools}>
Show Developer Tools
</ContextMenu.CheckboxItem>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Root> Radio #
Use ContextMenu.RadioItem for exclusive choices.
Right click here Long press here
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
let user = $state("pedro");
let theme = $state("light");
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Group>
<ContextMenu.Label>People</ContextMenu.Label>
<ContextMenu.RadioGroup bind:value={user}>
<ContextMenu.RadioItem value="pedro">Pedro Duarte</ContextMenu.RadioItem
>
<ContextMenu.RadioItem value="colm">Colm Tuite</ContextMenu.RadioItem>
</ContextMenu.RadioGroup>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Label>Theme</ContextMenu.Label>
<ContextMenu.RadioGroup bind:value={theme}>
<ContextMenu.RadioItem value="light">Light</ContextMenu.RadioItem>
<ContextMenu.RadioItem value="dark">Dark</ContextMenu.RadioItem>
<ContextMenu.RadioItem value="system">System</ContextMenu.RadioItem>
</ContextMenu.RadioGroup>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Root> Destructive #
Use variant="destructive" to style the menu item as destructive.
Right click here Long press here
<script lang="ts">
import PencilIcon from "@lucide/svelte/icons/pencil";
import ShareIcon from "@lucide/svelte/icons/share";
import TrashIcon from "@lucide/svelte/icons/trash";
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block">Right click here</span>
<span class="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Group>
<ContextMenu.Item>
<PencilIcon />
Edit
</ContextMenu.Item>
<ContextMenu.Item>
<ShareIcon />
Share
</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Group>
<ContextMenu.Item variant="destructive">
<TrashIcon />
Delete
</ContextMenu.Item>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Root> Sides #
Control submenu placement with side and align props.
Right click (top) Long press (top)
Right click (right) Long press (right)
Right click (bottom) Long press (bottom)
Right click (left) Long press (left)
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
const sides = ["top", "right", "bottom", "left"] as const;
</script>
<div class="grid w-full max-w-sm grid-cols-2 gap-4">
{#each sides as side (side)}
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm"
>
<span class="hidden pointer-fine:inline-block"
>Right click ({side})</span
>
<span class="hidden pointer-coarse:inline-block"
>Long press ({side})</span
>
</ContextMenu.Trigger>
<ContextMenu.Content {side}>
<ContextMenu.Group>
<ContextMenu.Item>Back</ContextMenu.Item>
<ContextMenu.Item>Forward</ContextMenu.Item>
<ContextMenu.Item>Reload</ContextMenu.Item>
</ContextMenu.Group>
</ContextMenu.Content>
</ContextMenu.Root>
{/each}
</div>