Context Menu
Displays a menu to the user — such as a set of actions or functions — triggered by right click.
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu/index.js";
let showBookmarks = $state(false);
let showFullURLs = $state(true);
let value = $state("pedro");
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed text-sm"
>
Right click here
</ContextMenu.Trigger>
<ContextMenu.Content class="w-52">
<ContextMenu.Item inset>
Back
<ContextMenu.Shortcut>⌘[</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item inset disabled>
Forward
<ContextMenu.Shortcut>⌘]</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item inset>
Reload
<ContextMenu.Shortcut>⌘R</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Sub>
<ContextMenu.SubTrigger inset>More Tools</ContextMenu.SubTrigger>
<ContextMenu.SubContent class="w-48">
<ContextMenu.Item>
Save Page As...
<ContextMenu.Shortcut>⇧⌘S</ContextMenu.Shortcut>
</ContextMenu.Item>
<ContextMenu.Item>Create Shortcut...</ContextMenu.Item>
<ContextMenu.Item>Name Window...</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item>Developer Tools</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
<ContextMenu.Separator />
<ContextMenu.CheckboxItem bind:checked={showBookmarks}
>Show Bookmarks</ContextMenu.CheckboxItem
>
<ContextMenu.CheckboxItem bind:checked={showFullURLs}
>Show Full URLs</ContextMenu.CheckboxItem
>
<ContextMenu.Separator />
<ContextMenu.RadioGroup bind:value>
<ContextMenu.Group>
<ContextMenu.GroupHeading inset>People</ContextMenu.GroupHeading>
<ContextMenu.RadioItem value="pedro">Pedro Duarte</ContextMenu.RadioItem
>
<ContextMenu.RadioItem value="colm">Colm Tuite</ContextMenu.RadioItem>
</ContextMenu.Group>
</ContextMenu.RadioGroup>
</ContextMenu.Content>
</ContextMenu.Root> Installation
pnpm dlx shadcn-svelte@latest add context-menu Install bits-ui:
pnpm add bits-ui -D Copy and paste the following code into your project.
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
import type { Snippet } from 'svelte';
import CheckIcon from 'phosphor-svelte/lib/Check';
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-2.5 border border-transparent py-2 pr-8 pl-3 font-mono text-xs font-medium uppercase tracking-[0.04em] text-zinc-300 outline-hidden select-none focus:border-[#b9d765]/50 focus:bg-[#18181b] focus:text-zinc-50 data-[checked=true]:text-[#d0e891] data-inset:pl-9.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="pointer-events-none absolute right-2 text-[#d0e891]">
{#if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</ContextMenuPrimitive.CheckboxItem>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
import ContextMenuPortal from './context-menu-portal.svelte';
import type { ComponentProps } from 'svelte';
import type { WithoutChildrenOrChild } from '$UTILS$.js';
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-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 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 z-50 min-w-48 overflow-x-hidden overflow-y-auto border border-[#222225] bg-[#09090b] p-1.5 text-zinc-50 shadow-none outline-none duration-100',
className
)}
{...restProps}
/>
</ContextMenuPortal>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn } from '$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 font-mono text-xs font-semibold uppercase tracking-[0.08em] text-zinc-400 data-inset:ps-8',
className
)}
{...restProps}
/>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: ContextMenuPrimitive.GroupProps = $props();
</script>
<ContextMenuPrimitive.Group
bind:ref
data-slot="context-menu-group"
class={cn('border-border/60 py-1 not-first:border-t', className)}
{...restProps}
/>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn } from '$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(
"relative flex cursor-default items-center gap-2.5 border border-transparent px-3 py-2 font-mono text-xs font-medium uppercase tracking-[0.04em] text-zinc-300 outline-hidden select-none focus:border-[#b9d765]/50 focus:bg-[#18181b] focus:text-zinc-50 focus:*:[svg]:text-[#d0e891] data-[variant=destructive]:text-[#f85149] data-[variant=destructive]:focus:bg-[#d1242f]/10 data-[variant=destructive]:focus:text-[#f85149] data-[variant=destructive]:*:[svg]:text-[#f85149]! data-inset:pl-9.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 group/context-menu-item",
className
)}
{...restProps}
/>
<script lang="ts">
import { cn, type WithElementRef } from '$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-3 py-2 font-mono text-xs font-semibold uppercase tracking-[0.08em] text-zinc-500 data-inset:pl-9.5 data-inset:pl-8',
className
)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
let { ...restProps }: ContextMenuPrimitive.PortalProps = $props();
</script>
<ContextMenuPrimitive.Portal {...restProps} />
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
value = $bindable(''),
class: className,
...restProps
}: ContextMenuPrimitive.RadioGroupProps = $props();
</script>
<ContextMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="context-menu-radio-group"
class={cn('border-border/60 py-1 not-first:border-t', className)}
{...restProps}
/>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn, type WithoutChild } from '$UTILS$.js';
import CheckIcon from 'phosphor-svelte/lib/Check';
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-2.5 border border-transparent py-2 pr-8 pl-3 font-mono text-xs font-medium uppercase tracking-[0.04em] text-zinc-300 outline-hidden select-none focus:border-[#b9d765]/50 focus:bg-[#18181b] focus:text-zinc-50 data-[checked=true]:text-[#d0e891] data-inset:pl-9.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="pointer-events-none absolute right-2 text-[#d0e891]">
{#if checked}
<CheckIcon />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</ContextMenuPrimitive.RadioItem>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn } from '$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.5 my-1.5 h-px bg-[#222225]', className)}
{...restProps}
/>
<script lang="ts">
import { cn, type WithElementRef } from '$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 font-mono text-[11px] uppercase tracking-[0.12em] text-zinc-500 group-focus/context-menu-item:text-[#d0e891]',
className
)}
{...restProps}
>
{@render children?.()}
</span>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn } from '$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-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 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 min-w-32 border border-[#222225] bg-[#09090b] p-1.5 text-zinc-50 shadow-none duration-100',
className
)}
{...restProps}
/>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn, type WithoutChild } from '$UTILS$.js';
import CaretRightIcon from 'phosphor-svelte/lib/CaretRight';
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 border border-transparent px-3 py-2 font-mono text-xs font-medium uppercase tracking-[0.04em] text-zinc-300 outline-hidden select-none focus:border-[#b9d765]/50 focus:bg-[#18181b] focus:text-zinc-50 data-open:border-[#b9d765]/50 data-open:bg-[#18181b] data-open:text-zinc-50 data-inset:pl-9.5 data-inset:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
className
)}
{...restProps}
>
{@render children?.()}
<CaretRightIcon class="ml-auto text-[#d0e891]" />
</ContextMenuPrimitive.SubTrigger>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.SubProps = $props();
</script>
<ContextMenuPrimitive.Sub bind:open {...restProps} />
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
import { cn } from '$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 outline-none focus-visible:ring-2 focus-visible:ring-ring/30',
className
)}
{...restProps}
/>
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: ContextMenuPrimitive.RootProps = $props();
</script>
<ContextMenuPrimitive.Root bind:open {...restProps} />
import Root from './context-menu.svelte';
import Sub from './context-menu-sub.svelte';
import Portal from './context-menu-portal.svelte';
import Trigger from './context-menu-trigger.svelte';
import Group from './context-menu-group.svelte';
import RadioGroup from './context-menu-radio-group.svelte';
import Item from './context-menu-item.svelte';
import GroupHeading from './context-menu-group-heading.svelte';
import Content from './context-menu-content.svelte';
import Shortcut from './context-menu-shortcut.svelte';
import RadioItem from './context-menu-radio-item.svelte';
import Separator from './context-menu-separator.svelte';
import SubContent from './context-menu-sub-content.svelte';
import SubTrigger from './context-menu-sub-trigger.svelte';
import CheckboxItem from './context-menu-checkbox-item.svelte';
import Label from './context-menu-label.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
};
Usage
<script lang="ts">
import * as ContextMenu from '$lib/components/ui/context-menu/index.js';
</script> <ContextMenu.Root>
<ContextMenu.Trigger>Right click</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>