Skip to content
edmi-ui

Context Menu

The same menu as Dropdown Menu, opened with a right-click or long-press on an area.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Right-click an index row
Right-click an index row
Right-click an index row
Terminal window
npx shadcn@latest add @edmi-ui/context-menu
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/context-menu
Terminal window
yarn dlx shadcn@latest add @edmi-ui/context-menu
Terminal window
bunx shadcn@latest add @edmi-ui/context-menu

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/context-menu
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/context-menu
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/context-menu
Terminal window
bunx shadcn-vue@latest add @edmi-ui/context-menu

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/context-menu.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/context-menu.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/context-menu.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/context-menu.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
<ContextMenu>
<ContextMenuTrigger className="flex h-40 w-72 select-none items-center justify-center rounded-lg border border-dashed border-border text-sm text-muted-foreground">
Right-click an index row
</ContextMenuTrigger>
<ContextMenuContent className="w-52">
<ContextMenuGroup>
<ContextMenuItem>Open</ContextMenuItem>
<ContextMenuItem>
Copy address <ContextMenuShortcut>⌘C</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>Share</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>Copy link</ContextMenuItem>
<ContextMenuItem>Share card</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">Hide index</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<script setup lang="ts">
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
</script>
<template>
<ContextMenu>
<ContextMenuTrigger
class="flex h-40 w-72 select-none items-center justify-center rounded-lg border border-dashed border-border text-sm text-muted-foreground"
>
Right-click an index row
</ContextMenuTrigger>
<ContextMenuContent class="w-52">
<ContextMenuGroup>
<ContextMenuItem>Open</ContextMenuItem>
<ContextMenuItem>
Copy address <ContextMenuShortcut>⌘C</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>Share</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>Copy link</ContextMenuItem>
<ContextMenuItem>Share card</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">Hide index</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</template>
<script lang="ts">
import * as ContextMenu from "$lib/components/ui/context-menu";
</script>
<ContextMenu.Root>
<ContextMenu.Trigger
class="flex h-40 w-72 select-none items-center justify-center rounded-lg border border-dashed border-border text-sm text-muted-foreground"
>
Right-click an index row
</ContextMenu.Trigger>
<ContextMenu.Content class="w-52">
<ContextMenu.Group>
<ContextMenu.Item>Open</ContextMenu.Item>
<ContextMenu.Item>
Copy address <ContextMenu.Shortcut>⌘C</ContextMenu.Shortcut>
</ContextMenu.Item>
</ContextMenu.Group>
<ContextMenu.Separator />
<ContextMenu.Sub>
<ContextMenu.SubTrigger>Share</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
<ContextMenu.Item>Copy link</ContextMenu.Item>
<ContextMenu.Item>Share card</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
<ContextMenu.Separator />
<ContextMenu.Item variant="destructive">Hide index</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
registry/ui/context-menu.tsx
"use client";
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu";
import { cn } from "cn";
import type * as React from "react";
import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// ✦ depth (v4): overlay role. Natural level is floating in layered mode; flat otherwise.
const overlayElevation = {
sunken: "",
flat: "",
raised: "border-transparent shadow-raised",
floating: "border-transparent shadow-floating",
};
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
}
function ContextMenuTrigger({
className,
...props
}: ContextMenuPrimitive.Trigger.Props) {
return (
<ContextMenuPrimitive.Trigger
data-slot="context-menu-trigger"
className={cn("select-none", className)}
{...props}
/>
);
}
function ContextMenuContent({
className,
align = "start",
alignOffset = 4,
side = "right",
sideOffset = 0,
elevation,
...props
}: ContextMenuPrimitive.Popup.Props &
Pick<
ContextMenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
> & {
/** ✦ depth: flat 0, raised +1 (bevel), floating +2 (bevel + drop). */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "overlay");
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<ContextMenuPrimitive.Popup
data-slot="context-menu-content"
className={cn(
"z-50 max-h-(--available-height) min-w-40 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-[closed]:overflow-hidden",
overlayElevation[level],
className,
)}
{...props}
/>
</ContextMenuPrimitive.Positioner>
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
}
function ContextMenuLabel({
className,
inset,
...props
}: ContextMenuPrimitive.GroupLabel.Props & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.GroupLabel
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: ContextMenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/menu-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:text-destructive-text data-[variant=destructive]:data-[highlighted]:bg-destructive-soft data-[variant=destructive]:data-[highlighted]:text-destructive-text data-[variant=destructive]:*:[svg]:text-destructive-text",
className,
)}
{...props}
/>
);
}
function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
return (
<ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: ContextMenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubmenuTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[popup-open]:bg-accent data-[popup-open]:text-accent-foreground data-[open]:bg-accent data-[open]:text-accent-foreground",
className,
)}
{...props}
>
{children}
<CaretRightIcon
className="rtl:rotate-180 ml-auto"
/>
</ContextMenuPrimitive.SubmenuTrigger>
);
}
function ContextMenuSubContent({
...props
}: React.ComponentProps<typeof ContextMenuContent>) {
return (
<ContextMenuContent
data-slot="context-menu-sub-content"
side="right"
{...props}
/>
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: ContextMenuPrimitive.CheckboxItem.Props & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center">
<ContextMenuPrimitive.CheckboxItemIndicator>
<CheckIcon
/>
</ContextMenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioGroup({
...props
}: ContextMenuPrimitive.RadioGroup.Props) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
);
}
function ContextMenuRadioItem({
className,
children,
inset,
...props
}: ContextMenuPrimitive.RadioItem.Props & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center">
<ContextMenuPrimitive.RadioItemIndicator>
<span className="block size-1.5 rounded-full bg-current" />
</ContextMenuPrimitive.RadioItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
function ContextMenuSeparator({
className,
...props
}: ContextMenuPrimitive.Separator.Props) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground",
className,
)}
{...props}
/>
);
}
export {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuPortal,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
};
registry/ui/context-menu/ContextMenu.vue
<script setup lang="ts">
import type { ContextMenuRootEmits, ContextMenuRootProps } from "reka-ui"
import { ContextMenuRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<ContextMenuRootProps>()
const emits = defineEmits<ContextMenuRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<ContextMenuRoot
data-slot="context-menu"
v-bind="forwarded"
>
<slot />
</ContextMenuRoot>
</template>
registry/ui/context-menu/ContextMenuCheckboxItem.vue
<script setup lang="ts">
import type { ContextMenuCheckboxItemEmits, ContextMenuCheckboxItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { Check } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
ContextMenuCheckboxItem,
ContextMenuItemIndicator,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<ContextMenuCheckboxItemProps & { class?: HTMLAttributes["class"], closeOnSelect?: boolean }>()
const emits = defineEmits<ContextMenuCheckboxItemEmits>()
const delegatedProps = reactiveOmit(props, "class", "closeOnSelect")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
// Checkbox and radio items keep the menu open (like Base UI and Bits); pass close-on-select to close it.
function keepOpen(event: Event) {
if (!props.closeOnSelect) event.preventDefault()
}
</script>
<template>
<ContextMenuCheckboxItem
data-slot="context-menu-checkbox-item"
v-bind="forwarded"
@select="keepOpen"
:class="cn(
`relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8`,
props.class,
)"
>
<span class="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center">
<ContextMenuItemIndicator>
<slot name="indicator-icon">
<Check />
</slot>
</ContextMenuItemIndicator>
</span>
<slot />
</ContextMenuCheckboxItem>
</template>
registry/ui/context-menu/ContextMenuContent.vue
<script setup lang="ts">
import type { ContextMenuContentEmits, ContextMenuContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ContextMenuContent,
ContextMenuPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<ContextMenuContentProps & { class?: HTMLAttributes["class"], elevation?: Elevation }>(), { elevation: undefined })
const emits = defineEmits<ContextMenuContentEmits>()
// ✦ depth (v4): overlay role. Natural level is floating in layered mode; flat otherwise.
const overlayElevation = {
sunken: '',
flat: '',
raised: 'border-transparent shadow-raised',
floating: 'border-transparent shadow-floating',
}
const level = useElevation(() => props.elevation, 'overlay')
const delegatedProps = reactiveOmit(props, "class", "elevation")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ContextMenuPortal>
<ContextMenuContent
data-slot="context-menu-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn(
`z-50 max-h-(--reka-context-menu-content-available-height) min-w-40 origin-(--reka-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 outline-none 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-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:overflow-hidden`,
overlayElevation[level],
props.class,
)"
>
<slot />
</ContextMenuContent>
</ContextMenuPortal>
</template>
registry/ui/context-menu/ContextMenuGroup.vue
<script setup lang="ts">
import type { ContextMenuGroupProps } from "reka-ui"
import { ContextMenuGroup } from "reka-ui"
const props = defineProps<ContextMenuGroupProps>()
</script>
<template>
<ContextMenuGroup
data-slot="context-menu-group"
v-bind="props"
>
<slot />
</ContextMenuGroup>
</template>
registry/ui/context-menu/ContextMenuItem.vue
<script setup lang="ts">
import type { ContextMenuItemEmits, ContextMenuItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ContextMenuItem,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<ContextMenuItemProps & {
class?: HTMLAttributes["class"]
inset?: boolean
variant?: "default" | "destructive"
}>(), {
variant: "default",
})
const emits = defineEmits<ContextMenuItemEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ContextMenuItem
data-slot="context-menu-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwarded"
:class="cn(
`group/menu-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:text-destructive-text data-[variant=destructive]:data-[highlighted]:bg-destructive-soft data-[variant=destructive]:data-[highlighted]:text-destructive-text data-[variant=destructive]:*:[svg]:text-destructive-text`,
props.class,
)"
>
<slot />
</ContextMenuItem>
</template>
registry/ui/context-menu/ContextMenuLabel.vue
<script setup lang="ts">
import type { ContextMenuLabelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ContextMenuLabel } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<ContextMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<ContextMenuLabel
data-slot="context-menu-label"
:data-inset="inset ? '' : undefined"
v-bind="delegatedProps"
:class="cn(
`px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground data-[inset]:pl-8`, props.class)"
>
<slot />
</ContextMenuLabel>
</template>
registry/ui/context-menu/ContextMenuPortal.vue
<script setup lang="ts">
import type { ContextMenuPortalProps } from "reka-ui"
import { ContextMenuPortal } from "reka-ui"
const props = defineProps<ContextMenuPortalProps>()
</script>
<template>
<ContextMenuPortal
data-slot="context-menu-portal"
v-bind="props"
>
<slot />
</ContextMenuPortal>
</template>
registry/ui/context-menu/ContextMenuRadioGroup.vue
<script setup lang="ts">
import type { ContextMenuRadioGroupEmits, ContextMenuRadioGroupProps } from "reka-ui"
import {
ContextMenuRadioGroup,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<ContextMenuRadioGroupProps>()
const emits = defineEmits<ContextMenuRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<ContextMenuRadioGroup
data-slot="context-menu-radio-group"
v-bind="forwarded"
>
<slot />
</ContextMenuRadioGroup>
</template>
registry/ui/context-menu/ContextMenuRadioItem.vue
<script setup lang="ts">
import type { ContextMenuRadioItemEmits, ContextMenuRadioItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ContextMenuItemIndicator,
ContextMenuRadioItem,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<ContextMenuRadioItemProps & { class?: HTMLAttributes["class"], closeOnSelect?: boolean }>()
const emits = defineEmits<ContextMenuRadioItemEmits>()
const delegatedProps = reactiveOmit(props, "class", "closeOnSelect")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
// Checkbox and radio items keep the menu open (like Base UI and Bits); pass close-on-select to close it.
function keepOpen(event: Event) {
if (!props.closeOnSelect) event.preventDefault()
}
</script>
<template>
<ContextMenuRadioItem
data-slot="context-menu-radio-item"
v-bind="forwarded"
@select="keepOpen"
:class="cn(
`relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8`,
props.class,
)"
>
<span class="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center">
<ContextMenuItemIndicator>
<slot name="indicator-icon">
<span class="block size-1.5 rounded-full bg-current" />
</slot>
</ContextMenuItemIndicator>
</span>
<slot />
</ContextMenuRadioItem>
</template>
registry/ui/context-menu/ContextMenuSeparator.vue
<script setup lang="ts">
import type { ContextMenuSeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ContextMenuSeparator,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<ContextMenuSeparatorProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<ContextMenuSeparator
data-slot="context-menu-separator"
v-bind="delegatedProps"
:class="cn(
`-mx-1 my-1 h-px bg-border`, props.class)"
/>
</template>
registry/ui/context-menu/ContextMenuShortcut.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
data-slot="context-menu-shortcut"
:class="cn(
`ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground`, props.class)"
>
<slot />
</span>
</template>
registry/ui/context-menu/ContextMenuSub.vue
<script setup lang="ts">
import type { ContextMenuSubEmits, ContextMenuSubProps } from "reka-ui"
import {
ContextMenuSub,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<ContextMenuSubProps>()
const emits = defineEmits<ContextMenuSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<ContextMenuSub
data-slot="context-menu-sub"
v-bind="forwarded"
>
<slot />
</ContextMenuSub>
</template>
registry/ui/context-menu/ContextMenuSubContent.vue
<script setup lang="ts">
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ContextMenuSubContent,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<ContextMenuSubContent
data-slot="context-menu-sub-content"
v-bind="forwarded"
:class="
cn(
`z-50 w-auto min-w-36 origin-(--reka-context-menu-content-transform-origin) overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 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-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95`,
props.class,
)
"
>
<slot />
</ContextMenuSubContent>
</template>
registry/ui/context-menu/ContextMenuSubTrigger.vue
<script setup lang="ts">
import type { ContextMenuSubTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { ChevronRight } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
ContextMenuSubTrigger,
useForwardProps,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<ContextMenuSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<ContextMenuSubTrigger
data-slot="context-menu-sub-trigger"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="cn(
`relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground`,
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto" />
</ContextMenuSubTrigger>
</template>
registry/ui/context-menu/ContextMenuTrigger.vue
<script setup lang="ts">
import type { ContextMenuTriggerProps } from "reka-ui"
import { ContextMenuTrigger, useForwardProps } from "reka-ui"
const props = defineProps<ContextMenuTriggerProps>()
const forwardedProps = useForwardProps(props)
</script>
<template>
<ContextMenuTrigger
data-slot="context-menu-trigger"
v-bind="forwardedProps"
>
<slot />
</ContextMenuTrigger>
</template>
registry/ui/context-menu/index.ts
export { default as ContextMenu } from "./ContextMenu.vue";
export { default as ContextMenuCheckboxItem } from "./ContextMenuCheckboxItem.vue";
export { default as ContextMenuContent } from "./ContextMenuContent.vue";
export { default as ContextMenuGroup } from "./ContextMenuGroup.vue";
export { default as ContextMenuItem } from "./ContextMenuItem.vue";
export { default as ContextMenuLabel } from "./ContextMenuLabel.vue";
export { default as ContextMenuRadioGroup } from "./ContextMenuRadioGroup.vue";
export { default as ContextMenuRadioItem } from "./ContextMenuRadioItem.vue";
export { default as ContextMenuSeparator } from "./ContextMenuSeparator.vue";
export { default as ContextMenuShortcut } from "./ContextMenuShortcut.vue";
export { default as ContextMenuSub } from "./ContextMenuSub.vue";
export { default as ContextMenuSubContent } from "./ContextMenuSubContent.vue";
export { default as ContextMenuSubTrigger } from "./ContextMenuSubTrigger.vue";
export { default as ContextMenuTrigger } from "./ContextMenuTrigger.vue";
src/lib/registry/ui/context-menu/context-menu-checkbox-item.svelte
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import CheckIcon from "phosphor-svelte/lib/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(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center">
{#if checked}
<CheckIcon
/>
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</ContextMenuPrimitive.CheckboxItem>
src/lib/registry/ui/context-menu/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 { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
import ContextMenuPortal from "./context-menu-portal.svelte";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
portalProps,
class: className,
elevation = "auto",
...restProps
}: ContextMenuPrimitive.ContentProps & {
/** ✦ depth: flat 0, raised +1 (bevel), floating +2 (bevel + drop). */
elevation?: Elevation;
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>;
} = $props();
// ✦ depth (v4): overlay role. Natural level is floating in layered mode; flat otherwise.
const overlayElevation = {
sunken: "",
flat: "",
raised: "border-transparent shadow-raised",
floating: "border-transparent shadow-floating",
};
const level = useElevation(() => elevation, "overlay");
</script>
<ContextMenuPortal {...portalProps}>
<ContextMenuPrimitive.Content
bind:ref
data-slot="context-menu-content"
class={cn(
"z-50 max-h-(--bits-context-menu-content-available-height) min-w-40 origin-(--bits-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 outline-none 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-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:overflow-hidden",
overlayElevation[level.current],
className
)}
{...restProps}
/>
</ContextMenuPortal>
src/lib/registry/ui/context-menu/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 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground data-[inset]:pl-8", className)}
{...restProps}
/>
src/lib/registry/ui/context-menu/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} />
src/lib/registry/ui/context-menu/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/menu-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:text-destructive-text data-[variant=destructive]:data-[highlighted]:bg-destructive-soft data-[variant=destructive]:data-[highlighted]:text-destructive-text data-[variant=destructive]:*:[svg]:text-destructive-text",
className
)}
{...restProps}
/>
src/lib/registry/ui/context-menu/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-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground data-[inset]:pl-8", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/context-menu/context-menu-portal.svelte
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
let { ...restProps }: ContextMenuPrimitive.PortalProps = $props();
</script>
<ContextMenuPrimitive.Portal {...restProps} />
src/lib/registry/ui/context-menu/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}
/>
src/lib/registry/ui/context-menu/context-menu-radio-item.svelte
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children: childrenProp,
closeOnSelect = false,
...restProps
}: WithoutChild<ContextMenuPrimitive.RadioItemProps> & {
inset?: boolean;
} = $props();
</script>
<ContextMenuPrimitive.RadioItem
bind:ref
{closeOnSelect}
data-slot="context-menu-radio-item"
data-inset={inset}
class={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center">
{#if checked}
<span class="size-1.5 rounded-full bg-current"></span>
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</ContextMenuPrimitive.RadioItem>
src/lib/registry/ui/context-menu/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}
/>
src/lib/registry/ui/context-menu/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 font-mono text-[11.5px] tracking-wide text-muted-foreground", className)}
{...restProps}
>
{@render children?.()}
</span>
src/lib/registry/ui/context-menu/context-menu-sub-content.svelte
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import ContextMenuPortal from "./context-menu-portal.svelte";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
align = "start",
alignOffset = -3,
portalProps,
...restProps
}: ContextMenuPrimitive.SubContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof ContextMenuPortal>>;
} = $props();
</script>
<ContextMenuPortal {...portalProps}>
<ContextMenuPrimitive.SubContent
bind:ref
data-slot="context-menu-sub-content"
{align}
{alignOffset}
class={cn(
"z-50 w-auto min-w-36 origin-(--bits-context-menu-content-transform-origin) overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 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-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className
)}
{...restProps}
/>
</ContextMenuPortal>
src/lib/registry/ui/context-menu/context-menu-sub-trigger.svelte
<script lang="ts">
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
import CaretRightIcon from "phosphor-svelte/lib/CaretRight";
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(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...restProps}
>
{@render children?.()}
<CaretRightIcon
class="ml-auto rtl:rotate-180"
/>
</ContextMenuPrimitive.SubTrigger>
src/lib/registry/ui/context-menu/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} />
src/lib/registry/ui/context-menu/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("select-none", className)}
{...restProps}
/>
src/lib/registry/ui/context-menu/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} />
src/lib/registry/ui/context-menu/index.ts
import Root from "./context-menu.svelte";
import CheckboxItem from "./context-menu-checkbox-item.svelte";
import Content from "./context-menu-content.svelte";
import Group from "./context-menu-group.svelte";
import GroupHeading from "./context-menu-group-heading.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 Sub from "./context-menu-sub.svelte";
import SubContent from "./context-menu-sub-content.svelte";
import SubTrigger from "./context-menu-sub-trigger.svelte";
import Trigger from "./context-menu-trigger.svelte";
export {
CheckboxItem,
CheckboxItem as ContextMenuCheckboxItem,
Content,
Content as ContextMenuContent,
Group,
Group as ContextMenuGroup,
GroupHeading,
GroupHeading as ContextMenuGroupHeading,
Item,
Item as ContextMenuItem,
Label,
Label as ContextMenuLabel,
Portal,
Portal as ContextMenuPortal,
RadioGroup,
RadioGroup as ContextMenuRadioGroup,
RadioItem,
RadioItem as ContextMenuRadioItem,
Root,
//
Root as ContextMenu,
Separator,
Separator as ContextMenuSeparator,
Shortcut,
Shortcut as ContextMenuShortcut,
Sub,
Sub as ContextMenuSub,
SubContent,
SubContent as ContextMenuSubContent,
SubTrigger,
SubTrigger as ContextMenuSubTrigger,
Trigger,
Trigger as ContextMenuTrigger,
};

See the Elevation guide for the levels, roles and layered mode.

Flat by default. On ContextMenuContent, raised (+1) adds the bevel and floating (+2) the bevel plus one soft drop.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Right-click: Flat
Right-click: Raised (+1)
Right-click: Floating (+2)
Right-click: Flat Right-click: Raised (+1) Right-click: Floating (+2)
Right-click: Flat
Right-click: Raised (+1)
Right-click: Floating (+2)
Prop Type Default
ContextMenuContent elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"

Same parts and props as Dropdown Menu, prefixed ContextMenu: ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuGroup, ContextMenuLabel, ContextMenuItem (variant default | destructive), ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent.

  • Same recipe as Dropdown Menu: data-[highlighted] items, left-hand indicators, optional elevation on the content.