A comprehensive context menu component library for Vue 3 applications with themes, keyboard navigation, and programmatic control
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
The component API provides Vue components for declarative context menu construction. This approach integrates seamlessly with Vue templates and is ideal for static menus, form-based interfaces, and component-driven architectures.
Main context menu container component that manages menu display, positioning, and lifecycle.
declare component ContextMenu {
props: {
/** Menu configuration options */
options: MenuOptions;
/** Show/hide menu state */
show: boolean;
};
emits: {
/** Emitted when show state should change */
'update:show': (show: boolean) => boolean;
/** Emitted when menu closes */
'close': () => boolean;
};
slots: {
/** Default slot for menu items */
default: {};
/** Custom item renderer slot */
itemRender: {
label: string;
icon: string;
disabled: boolean;
onClick: (e: MouseEvent) => void;
onMouseEnter: (e: MouseEvent) => void;
};
/** Custom separator renderer slot */
separatorRender: {};
/** Custom icon renderer slot */
itemIconRender: { label: string; icon: string; };
/** Custom label renderer slot */
itemLabelRender: { label: string; };
/** Custom shortcut renderer slot */
itemShortcutRender: { shortcut: string; };
/** Custom right arrow renderer slot */
itemRightArrowRender: {};
/** Custom check mark renderer slot */
itemCheckRender: { checked: boolean; };
};
}Usage Examples:
<template>
<!-- Basic component usage -->
<div @contextmenu="onRightClick">
<context-menu v-model:show="showMenu" :options="menuOptions">
<context-menu-item label="Copy" icon="copy" @click="handleCopy" />
<context-menu-item label="Paste" icon="paste" @click="handlePaste" />
<context-menu-separator />
<context-menu-item label="Delete" icon="delete" @click="handleDelete" />
</context-menu>
</div>
<!-- Advanced usage with nested menus -->
<div @contextmenu="onAdvancedRightClick">
<context-menu
v-model:show="showAdvancedMenu"
:options="advancedOptions"
@close="onMenuClose"
>
<context-menu-item label="File Operations" icon="folder">
<template #submenu="{ show }">
<context-menu-group v-if="show" label="File">
<context-menu-item label="New" shortcut="Ctrl+N" @click="createNew" />
<context-menu-item label="Open" shortcut="Ctrl+O" @click="openFile" />
<context-menu-separator />
<context-menu-item label="Save" shortcut="Ctrl+S" @click="saveFile" />
</context-menu-group>
</template>
</context-menu-item>
<context-menu-item label="Edit" :disabled="!canEdit" @click="editItem" />
</context-menu>
</div>
<!-- Custom rendering with slots -->
<context-menu v-model:show="showCustomMenu" :options="customOptions">
<template #itemRender="{ label, icon, onClick, onMouseEnter, disabled }">
<div
:class="['custom-menu-item', { disabled }]"
@click="onClick"
@mouseenter="onMouseEnter"
>
<img v-if="icon" :src="icon" class="custom-icon" />
<span class="custom-label">{{ label }}</span>
</div>
</template>
<context-menu-item label="Custom Item" icon="/icons/custom.png" />
</context-menu>
</template>
<script setup>
import { ref } from 'vue';
const showMenu = ref(false);
const menuOptions = ref({
x: 0,
y: 0,
theme: 'default',
zIndex: 1000
});
function onRightClick(e) {
e.preventDefault();
menuOptions.value.x = e.clientX;
menuOptions.value.y = e.clientY;
showMenu.value = true;
}
function onMenuClose() {
console.log('Menu closed');
}
</script>Individual menu item component with support for icons, labels, shortcuts, and click handling.
declare component ContextMenuItem {
props: {
/** Menu item label text, VNode, or render function */
label?: string | VNode | ((label: string) => VNode);
/** Icon CSS class, VNode, or render function */
icon?: string | VNode | ((icon: string) => VNode);
/** Custom icon font class name */
iconFontClass?: string;
/** SVG symbol icon reference */
svgIcon?: string;
/** SVG element properties */
svgProps?: SVGAttributes;
/** Disable the menu item */
disabled?: boolean;
/** Hide the menu item */
hidden?: boolean;
/** Show check mark */
checked?: boolean;
/** Shortcut key display text */
shortcut?: string;
/** Reserve icon width space */
preserveIconWidth?: boolean;
/** Custom CSS class */
customClass?: string;
/** Custom render function */
customRender?: VNode | ((item: MenuItem) => VNode);
/** Click handler function */
clickHandler?: (e: MouseEvent | KeyboardEvent) => void;
/** Show right arrow indicator for submenus */
showRightArrow?: boolean;
/** Indicates if item has children/submenu */
hasChildren?: boolean;
/** Should close menu when this item is clicked */
clickClose?: boolean;
/** Allow click event when item has children */
clickableWhenHasChildren?: boolean;
/** Raw MenuItem data object */
rawMenuItem?: MenuItem;
};
emits: {
/** Emitted when item is clicked */
'click': (e: MouseEvent | KeyboardEvent) => boolean;
/** Emitted when submenu opens */
'subMenuOpen': () => boolean;
/** Emitted when submenu closes */
'subMenuClose': () => boolean;
};
slots: {
/** Default slot content */
default: {};
/** Icon slot */
icon: {};
/** Label slot */
label: {};
/** Shortcut slot */
shortcut: {};
/** Right arrow slot for submenus */
rightArrow: {};
/** Check mark slot */
check: {};
/** Submenu slot */
submenu: { context: MenuItemContext; show: boolean; };
};
}Usage Examples:
<template>
<!-- Basic menu items -->
<context-menu-item label="Simple Item" @click="handleClick" />
<!-- Item with icon and shortcut -->
<context-menu-item
label="Save File"
icon="save-icon"
shortcut="Ctrl+S"
@click="saveFile"
/>
<!-- Disabled and checked items -->
<context-menu-item
label="Readonly Mode"
:disabled="!canEdit"
:checked="isReadonly"
@click="toggleReadonly"
/>
<!-- Item with custom icon slot -->
<context-menu-item label="Custom Icon" @click="handleCustom">
<template #icon>
<svg class="custom-svg-icon">
<use xlink:href="#my-icon"></use>
</svg>
</template>
</context-menu-item>
<!-- Item with submenu -->
<context-menu-item label="Recent Files">
<template #submenu="{ show, context }">
<context-menu-group v-if="show" label="Recent">
<context-menu-item
v-for="file in recentFiles"
:key="file.id"
:label="file.name"
@click="() => openFile(file)"
/>
</context-menu-group>
</template>
</context-menu-item>
<!-- Item with custom rendering -->
<context-menu-item
label="Custom Render"
:custom-render="customItemRenderer"
@click="handleCustomRender"
/>
</template>
<script setup>
import { h } from 'vue';
function customItemRenderer(item) {
return h('div', { class: 'custom-item-wrapper' }, [
h('span', { class: 'custom-prefix' }, '★'),
h('span', item.label),
h('span', { class: 'custom-suffix' }, '→')
]);
}
</script>Visual separator component for grouping related menu items.
declare component ContextMenuSeparator {
// No props or events
}Usage:
<template>
<context-menu>
<context-menu-item label="Copy" @click="copy" />
<context-menu-item label="Paste" @click="paste" />
<!-- Visual separator -->
<context-menu-separator />
<context-menu-item label="Delete" @click="delete" />
</context-menu>
</template>Container component for creating nested submenus with optional group labels.
declare component ContextMenuGroup {
props: {
/** Group label text */
label?: string;
/** Icon CSS class or image path */
icon?: string;
/** Custom icon font class name */
iconFontClass?: string;
/** SVG symbol icon reference */
svgIcon?: string;
/** SVG element properties */
svgProps?: SVGAttributes;
/** Disable the group */
disabled?: boolean;
/** Hide the group */
hidden?: boolean;
/** Show check mark */
checked?: boolean;
/** Shortcut key display text */
shortcut?: string;
/** Reserve fixed-width icon area */
preserveIconWidth?: boolean;
/** Show right arrow indicator */
showRightArrow?: boolean;
/** Close menu when group is clicked */
clickClose?: boolean;
/** Adjust submenu position */
adjustSubMenuPosition?: boolean;
/** Maximum height of submenu */
maxHeight?: number | string;
/** Maximum width of submenu */
maxWidth?: number | string;
/** Minimum width of submenu */
minWidth?: number | string;
/** Custom CSS class */
customClass?: string;
/** Click handler function */
clickHandler?: () => void;
};
slots: {
/** Default slot for child menu items */
default: {};
};
}Usage Examples:
<template>
<!-- Basic group with label -->
<context-menu-group label="File Operations">
<context-menu-item label="New" @click="createNew" />
<context-menu-item label="Open" @click="openFile" />
<context-menu-item label="Save" @click="saveFile" />
</context-menu-group>
<!-- Nested groups -->
<context-menu-group label="Edit">
<context-menu-item label="Undo" @click="undo" />
<context-menu-item label="Redo" @click="redo" />
<context-menu-group label="Selection">
<context-menu-item label="Select All" @click="selectAll" />
<context-menu-item label="Select None" @click="selectNone" />
<context-menu-item label="Invert Selection" @click="invertSelection" />
</context-menu-group>
</context-menu-group>
<!-- Dynamic group content -->
<context-menu-group label="Recent Projects">
<context-menu-item
v-for="project in recentProjects"
:key="project.id"
:label="project.name"
:icon="project.icon"
@click="() => openProject(project)"
/>
<context-menu-separator v-if="recentProjects.length > 0" />
<context-menu-item label="Clear Recent" @click="clearRecent" />
</context-menu-group>
</template><template>
<div class="editor" @contextmenu="showContextMenu">
<context-menu v-model:show="menuVisible" :options="menuConfig">
<context-menu-item
label="Cut"
:disabled="!hasSelection"
shortcut="Ctrl+X"
@click="cutText"
/>
<context-menu-item
label="Copy"
:disabled="!hasSelection"
shortcut="Ctrl+C"
@click="copyText"
/>
<context-menu-item
label="Paste"
:disabled="!canPaste"
shortcut="Ctrl+V"
@click="pasteText"
/>
<context-menu-separator />
<context-menu-item
label="Select All"
shortcut="Ctrl+A"
@click="selectAllText"
/>
</context-menu>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const menuVisible = ref(false);
const selectedText = ref('');
const clipboard = ref('');
const menuConfig = ref({
x: 0,
y: 0,
theme: 'default'
});
const hasSelection = computed(() => selectedText.value.length > 0);
const canPaste = computed(() => clipboard.value.length > 0);
function showContextMenu(e) {
e.preventDefault();
menuConfig.value.x = e.clientX;
menuConfig.value.y = e.clientY;
menuVisible.value = true;
}
</script><template>
<form @submit="submitForm">
<input
v-model="formData.name"
@contextmenu="showFieldMenu"
data-field="name"
/>
<context-menu v-model:show="fieldMenuVisible" :options="fieldMenuConfig">
<context-menu-item label="Clear Field" @click="clearCurrentField" />
<context-menu-item label="Reset to Default" @click="resetCurrentField" />
<context-menu-separator />
<context-menu-item label="Validate Field" @click="validateCurrentField" />
<context-menu-group label="Auto-fill">
<context-menu-item
v-for="suggestion in currentFieldSuggestions"
:key="suggestion"
:label="suggestion"
@click="() => setFieldValue(suggestion)"
/>
</context-menu-group>
</context-menu>
</form>
</template>
<script setup>
import { ref, computed } from 'vue';
const fieldMenuVisible = ref(false);
const currentField = ref('');
const formData = ref({ name: '', email: '' });
const currentFieldSuggestions = computed(() => {
if (currentField.value === 'name') {
return ['John Doe', 'Jane Smith', 'Admin User'];
}
return [];
});
function showFieldMenu(e) {
e.preventDefault();
currentField.value = e.target.dataset.field;
fieldMenuConfig.value.x = e.clientX;
fieldMenuConfig.value.y = e.clientY;
fieldMenuVisible.value = true;
}
</script>When using template refs with context menu components:
interface ContextMenuComponentRef extends ContextMenuInstance {
/** Close the menu */
closeMenu(): void;
/** Check if menu is closed */
isClosed(): boolean;
/** Get menu reference for advanced control */
getMenuRef(): ContextSubMenuInstance | undefined;
/** Get menu dimensions */
getMenuDimensions(): { width: number, height: number };
}
interface ContextMenuGroupRef {
/** Get submenu instance */
getSubMenuRef(): ContextSubMenuInstance;
/** Get menu item reference */
getMenuItemRef(): ContextSubMenuInstance;
}Usage with Template Refs:
<template>
<context-menu ref="menuRef" v-model:show="show" :options="options">
<!-- menu items -->
</context-menu>
<button @click="controlMenu">Control Menu</button>
</template>
<script setup>
import { ref } from 'vue';
const menuRef = ref<ContextMenuComponentRef>();
function controlMenu() {
if (menuRef.value) {
if (menuRef.value.isClosed()) {
// Menu is closed, could trigger show
} else {
// Menu is open, close it
menuRef.value.closeMenu();
}
}
}
</script>