Right-click here
<script setup lang="ts">
import { ref } from 'vue'
import { Copy, FolderInput, Pencil, Trash2 } from '@lucide/vue'
import {
Center,
ContextMenu,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
Stack,
Text,
} from '@hina-ui/vue'
const last = ref('')
</script>
<template>
<Stack gap="sm" align="stretch" class="w-96">
<ContextMenu label="File actions">
<Center class="bg-inset h-32 rounded-lg border border-dashed select-none">
<Text tone="muted" size="sm">Right-click here</Text>
</Center>
<template #content>
<ContextMenuItem @select="last = 'Rename'">
<template #icon><Pencil /></template>
Rename
</ContextMenuItem>
<ContextMenuItem @select="last = 'Copy'">
<template #icon><Copy /></template>
Copy
</ContextMenuItem>
<ContextMenuSub label="Move to">
<template #icon><FolderInput /></template>
<ContextMenuItem @select="last = 'Move to Favorites'">Favorites</ContextMenuItem>
<ContextMenuItem @select="last = 'Move to Archive'">Archive</ContextMenuItem>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem tone="danger" @select="last = 'Delete'">
<template #icon><Trash2 /></template>
Delete
</ContextMenuItem>
</template>
</ContextMenu>
<Text v-if="last" tone="muted" size="sm">Picked: {{ last }}</Text>
</Stack>
</template>
Usage
import { ContextMenu, ContextMenuItem } from '@hina-ui/vue'
The default slot is the area that answers a right-click, and the content slot holds the items. Right-click inside the area, or long-press on a touch screen, and the menu opens at the pointer; picking an item closes it. Items, checkbox items, radio items, groups, labels, separators and submenus are the same set as in DropdownMenu, only named with the ContextMenu prefix.
Right-click here
<script setup lang="ts">
import { Center, ContextMenu, ContextMenuItem, Text } from '@hina-ui/vue'
</script>
<template>
<ContextMenu label="Image actions">
<Center class="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
<Text tone="muted" size="sm">Right-click here</Text>
</Center>
<template #content>
<ContextMenuItem>Open in new tab</ContextMenuItem>
<ContextMenuItem>Copy image address</ContextMenuItem>
<ContextMenuItem>Save image</ContextMenuItem>
</template>
</ContextMenu>
</template>
Examples
Items
The icon slot of an item holds a leading icon and trailing the trailing content; give irreversible actions such as delete tone="danger". ContextMenuLabel is a title that cannot be selected, and ContextMenuSeparator draws a line.
Right-click here
<script setup lang="ts">
import { Copy, Pencil, Trash2 } from '@lucide/vue'
import {
Center,
ContextMenu,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
Kbd,
Text,
} from '@hina-ui/vue'
</script>
<template>
<ContextMenu label="Note actions">
<Center class="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
<Text tone="muted" size="sm">Right-click here</Text>
</Center>
<template #content>
<ContextMenuLabel>This note</ContextMenuLabel>
<ContextMenuItem>
<template #icon><Pencil /></template>
Edit
<template #trailing><Kbd>E</Kbd></template>
</ContextMenuItem>
<ContextMenuItem>
<template #icon><Copy /></template>
Copy
<template #trailing><Kbd>⌘C</Kbd></template>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem tone="danger">
<template #icon><Trash2 /></template>
Delete
</ContextMenuItem>
</template>
</ContextMenu>
</template>
Checkbox items
ContextMenuCheckboxItem is a toggle where several can be on at once; checked supports two-way binding, and the menu stays open after toggling.
Grid on, ruler off
<script setup lang="ts">
import { ref } from 'vue'
import { Center, ContextMenu, ContextMenuCheckboxItem, Text } from '@hina-ui/vue'
const grid = ref(true)
const ruler = ref(false)
</script>
<template>
<ContextMenu label="View">
<Center class="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
<Text tone="muted" size="sm">
Grid {{ grid ? 'on' : 'off' }}, ruler {{ ruler ? 'on' : 'off' }}
</Text>
</Center>
<template #content>
<ContextMenuCheckboxItem v-model:checked="grid">Show grid</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem v-model:checked="ruler">Show ruler</ContextMenuCheckboxItem>
</template>
</ContextMenu>
</template>
Radio items
Wrap a set of mutually exclusive options in ContextMenuRadioGroup; the current one carries the check mark.
Sorted by name
<script setup lang="ts">
import { ref } from 'vue'
import {
Center,
ContextMenu,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
Text,
} from '@hina-ui/vue'
const sort = ref('name')
const names: Record<string, string> = { name: 'name', date: 'date modified', size: 'size' }
</script>
<template>
<ContextMenu label="Sort">
<Center class="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
<Text tone="muted" size="sm">Sorted by {{ names[sort] }}</Text>
</Center>
<template #content>
<ContextMenuLabel>Sort by</ContextMenuLabel>
<ContextMenuRadioGroup v-model="sort">
<ContextMenuRadioItem value="name">Name</ContextMenuRadioItem>
<ContextMenuRadioItem value="date">Date modified</ContextMenuRadioItem>
<ContextMenuRadioItem value="size">Size</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</template>
</ContextMenu>
</template>
Disabled
disabled stops the whole area from answering a right-click, so the browser’s own menu shows as usual; disabled on an item only makes that item unselectable.
Right-click here
<script setup lang="ts">
import { ref } from 'vue'
import { Center, ContextMenu, ContextMenuItem, Stack, Switch, Text } from '@hina-ui/vue'
const locked = ref(false)
</script>
<template>
<Stack gap="md" align="start">
<Switch v-model="locked">Lock the area</Switch>
<ContextMenu label="Item actions" :disabled="locked">
<Center class="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
<Text tone="muted" size="sm">
{{ locked ? "Locked; right-click shows the browser's own menu" : 'Right-click here' }}
</Text>
</Center>
<template #content>
<ContextMenuItem>Edit</ContextMenuItem>
<ContextMenuItem disabled>Move</ContextMenuItem>
<ContextMenuItem tone="danger">Delete</ContextMenuItem>
</template>
</ContextMenu>
</Stack>
</template>
Behavior
- The menu opens at the pointer and flips inward near the viewport edge.
- The page stops scrolling while the menu is open; a click outside, Esc or picking an item closes it.
- Arrow keys move between items, Enter or Space picks one, and typing a letter jumps to a matching item.
- A long press opens the menu on touch screens.
Accessibility
- The menu is
role="menu", named throughlabel; items aremenuitem,menuitemcheckboxandmenuitemradio. - Actions in a context menu should also be reachable elsewhere on the page, since keyboard and assistive-technology users may not be able to trigger it.
API
ContextMenu
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Accessible name of the menu |
disabled | boolean | false | Whether the area stops answering a right-click |
open | boolean | — | Whether it is open, supports two-way binding |
class | string | — | Classes appended to the menu panel |
| Slot | Description |
|---|---|
| default | The area that answers right-click |
content | The items of the menu |
Items and other parts
ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuGroup, ContextMenuLabel, ContextMenuSeparator and ContextMenuSub take exactly the props, slots and events of their DropdownMenu counterparts; see DropdownMenu.