Components

Context Menu

A composable context menu opened by right-clicking on desktop or long-pressing on mobile, with nested submenus, selectable items, shortcuts, and icon-friendly actions.

Preview

Right-click on desktop or long-press on mobile

More examples

Icons and nested submenus

Compose multiple submenu levels and render tree-shakeable icon components directly inside actions.

Campaign assets

12 items

Right-click on desktop or long-press on mobile

Latest action

No action selected yet.

Checkboxes and radio groups

Build stateful view controls with checked items, exclusive choices, groups, and disabled actions.

Recent projects

Right-click on desktop or long-press on mobile to change this view

Design system

Updated 8 minutes ago

Active

Mobile app

Updated yesterday

Review

Documentation

Updated 3 days ago

Draft

Comfortable density · Details shown · Compact rows off

Installation

Install directly from the hosted registry with the shadcn-svelte CLI.

Terminal
npx shadcn-svelte@latest add https://ui.akcodeworks.com/r/context-menu.json

Usage

Compose Root, Trigger, Content, and Item using the ContextMenu namespace. Tell users to right-click the trigger on desktop or long-press it on mobile. Add Sub, SubTrigger, and SubContent for any depth of nested menu. CheckboxItem and RadioGroup provide persistent selection state through Svelte bindings. For action icons, import components from the icon library configured by shadcn and render them directly inside an Item or SubTrigger; the menu automatically sizes and aligns SVG icons.

API reference

PropTypeDefaultDescription
Root.openboolean (bindable)falseControls whether the root menu is open.
TriggerBits UI trigger props—Element that opens the component through its supported interaction.
ContentBits UI content props—Floating surface that contains the component content.
Item.variantdefault | destructivedefaultSelects the visual treatment for an individual item.
Item.insetbooleanfalseAdds leading inset spacing to align an item with icon-bearing actions.
CheckboxItem.checkedboolean (bindable)falseControls whether the checkbox item is selected.
RadioGroup.valuestring (bindable)''Value of the currently selected radio item.
Sub.openboolean (bindable)falseControls whether the nested submenu is open.
classstring—Tailwind CSS classes applied to the component root.

Source

These files are included in the registry item and copied into the consuming project.

context-menu.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.RootProps = $props();</script><ContextMenuPrimitive.Root bind:open {...restProps} />
context-menu-trigger.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	import { cn } from '$lib/utils.js';	let {		ref = $bindable(null),		class: className,		...restProps	}: ContextMenuPrimitive.TriggerProps = $props();</script><ContextMenuPrimitive.Trigger	bind:ref	data-slot="context-menu-trigger"	class={cn('cn-context-menu-trigger select-none', className)}	{...restProps}/>
context-menu-content.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	import { cn } from '$lib/utils.js';	import type { WithoutChildrenOrChild } from '$lib/utils.js';	import ContextMenuPortal from './context-menu-portal.svelte';	import type { ComponentProps } from 'svelte';	let {		ref = $bindable(null),		portalProps,		class: className,		...restProps	}: ContextMenuPrimitive.ContentProps & {		portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>;	} = $props();</script><ContextMenuPortal {...portalProps}>	<ContextMenuPrimitive.Content		bind:ref		data-slot="context-menu-content"		class={cn(			'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 min-w-36 overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none',			className		)}		{...restProps}	/></ContextMenuPortal>
context-menu-item.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	import { cn } from '$lib/utils.js';	let {		ref = $bindable(null),		class: className,		inset,		variant = 'default',		...restProps	}: ContextMenuPrimitive.ItemProps & {		inset?: boolean;		variant?: 'default' | 'destructive';	} = $props();</script><ContextMenuPrimitive.Item	bind:ref	data-slot="context-menu-item"	data-inset={inset}	data-variant={variant}	class={cn(		"group/context-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 focus:*:[svg]:text-accent-foreground data-[variant=destructive]:*:[svg]:text-destructive",		className	)}	{...restProps}/>
context-menu-checkbox-item.svelte
<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,		...restProps	}: WithoutChildrenOrChild<ContextMenuPrimitive.CheckboxItemProps> & {		inset?: boolean;		children?: Snippet;	} = $props();</script><ContextMenuPrimitive.CheckboxItem	bind:ref	bind:checked	bind:indeterminate	data-slot="context-menu-checkbox-item"	data-inset={inset}	class={cn(		"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",		className	)}	{...restProps}>	{#snippet children({ checked })}		<span class="pointer-events-none absolute right-2">			{#if checked}				<CheckIcon />			{/if}		</span>		{@render childrenProp?.()}	{/snippet}</ContextMenuPrimitive.CheckboxItem>
context-menu-radio-group.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	let {		ref = $bindable(null),		value = $bindable(''),		...restProps	}: ContextMenuPrimitive.RadioGroupProps = $props();</script><ContextMenuPrimitive.RadioGroup	bind:ref	bind:value	data-slot="context-menu-radio-group"	{...restProps}/>
context-menu-radio-item.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	import CheckIcon from '@lucide/svelte/icons/check';	import { cn, type WithoutChild } from '$lib/utils.js';	let {		ref = $bindable(null),		class: className,		inset,		children: childrenProp,		...restProps	}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & {		inset?: boolean;	} = $props();</script><ContextMenuPrimitive.RadioItem	bind:ref	data-slot="context-menu-radio-item"	data-inset={inset}	class={cn(		"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",		className	)}	{...restProps}>	{#snippet children({ checked })}		<span class="pointer-events-none absolute right-2">			{#if checked}				<CheckIcon />			{/if}		</span>		{@render childrenProp?.({ checked })}	{/snippet}</ContextMenuPrimitive.RadioItem>
context-menu-group.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	let { ref = $bindable(null), ...restProps }: ContextMenuPrimitive.GroupProps = $props();</script><ContextMenuPrimitive.Group bind:ref data-slot="context-menu-group" {...restProps} />
context-menu-group-heading.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	import { cn } from '$lib/utils.js';	let {		ref = $bindable(null),		class: className,		inset,		...restProps	}: ContextMenuPrimitive.GroupHeadingProps & {		inset?: boolean;	} = $props();</script><ContextMenuPrimitive.GroupHeading	bind:ref	data-slot="context-menu-group-heading"	data-inset={inset}	class={cn('px-2 py-1.5 text-sm font-medium text-foreground data-inset:ps-8', className)}	{...restProps}/>
context-menu-label.svelte
<script lang="ts">	import { cn, type WithElementRef } from '$lib/utils.js';	import type { HTMLAttributes } from 'svelte/elements';	let {		ref = $bindable(null),		class: className,		inset,		children,		...restProps	}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {		inset?: boolean;	} = $props();</script><div	bind:this={ref}	data-slot="context-menu-label"	data-inset={inset}	class={cn(		'px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7 data-inset:pl-8',		className	)}	{...restProps}>	{@render children?.()}</div>
context-menu-shortcut.svelte
<script lang="ts">	import { cn, type WithElementRef } from '$lib/utils.js';	import type { HTMLAttributes } from 'svelte/elements';	let {		ref = $bindable(null),		class: className,		children,		...restProps	}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();</script><span	bind:this={ref}	data-slot="context-menu-shortcut"	class={cn(		'ml-auto text-xs tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground',		className	)}	{...restProps}>	{@render children?.()}</span>
context-menu-separator.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	import { cn } from '$lib/utils.js';	let {		ref = $bindable(null),		class: className,		...restProps	}: ContextMenuPrimitive.SeparatorProps = $props();</script><ContextMenuPrimitive.Separator	bind:ref	data-slot="context-menu-separator"	class={cn('-mx-1 my-1 h-px bg-border', className)}	{...restProps}/>
context-menu-sub.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.SubProps = $props();</script><ContextMenuPrimitive.Sub bind:open {...restProps} />
context-menu-sub-trigger.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';	import { cn, type WithoutChild } from '$lib/utils.js';	let {		ref = $bindable(null),		class: className,		inset,		children,		...restProps	}: WithoutChild<ContextMenuPrimitive.SubTriggerProps> & {		inset?: boolean;	} = $props();</script><ContextMenuPrimitive.SubTrigger	bind:ref	data-slot="context-menu-sub-trigger"	data-inset={inset}	class={cn(		"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:ps-8 data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",		className	)}	{...restProps}>	{@render children?.()}	<ChevronRightIcon class="ml-auto" /></ContextMenuPrimitive.SubTrigger>
context-menu-sub-content.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	import { cn } from '$lib/utils.js';	let {		ref = $bindable(null),		class: className,		...restProps	}: ContextMenuPrimitive.SubContentProps = $props();</script><ContextMenuPrimitive.SubContent	bind:ref	data-slot="context-menu-sub-content"	class={cn(		'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 min-w-32 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg duration-100',		className	)}	{...restProps}/>
context-menu-portal.svelte
<script lang="ts">	import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';	let { ...restProps }: ContextMenuPrimitive.PortalProps = $props();</script><ContextMenuPrimitive.Portal {...restProps} />
index.ts
import CheckboxItem from './context-menu-checkbox-item.svelte';import Content from './context-menu-content.svelte';import GroupHeading from './context-menu-group-heading.svelte';import Group from './context-menu-group.svelte';import Item from './context-menu-item.svelte';import Label from './context-menu-label.svelte';import Portal from './context-menu-portal.svelte';import RadioGroup from './context-menu-radio-group.svelte';import RadioItem from './context-menu-radio-item.svelte';import Separator from './context-menu-separator.svelte';import Shortcut from './context-menu-shortcut.svelte';import SubContent from './context-menu-sub-content.svelte';import SubTrigger from './context-menu-sub-trigger.svelte';import Sub from './context-menu-sub.svelte';import Trigger from './context-menu-trigger.svelte';import Root from './context-menu.svelte';export {	Root,	Sub,	Portal,	Item,	GroupHeading,	Label,	Group,	Trigger,	Content,	Shortcut,	Separator,	RadioItem,	SubContent,	SubTrigger,	RadioGroup,	CheckboxItem,	//	Root as ContextMenu,	Sub as ContextMenuSub,	Portal as ContextMenuPortal,	Item as ContextMenuItem,	GroupHeading as ContextMenuGroupHeading,	Group as ContextMenuGroup,	Content as ContextMenuContent,	Trigger as ContextMenuTrigger,	Shortcut as ContextMenuShortcut,	RadioItem as ContextMenuRadioItem,	Separator as ContextMenuSeparator,	RadioGroup as ContextMenuRadioGroup,	SubContent as ContextMenuSubContent,	SubTrigger as ContextMenuSubTrigger,	CheckboxItem as ContextMenuCheckboxItem,	Label as ContextMenuLabel};