hcg-context-menu
v1.0.0
Published
Lightweight dependency-free JavaScript context menu and dropdown library with submenus, icons, headers, keyboard navigation, mobile long-press, viewport flipping, and dark mode.
Downloads
143
Maintainers
Readme
hcg-context-menu
A lightweight, dependency-free JavaScript library for building right-click context menus and click dropdown menus. Supports nested submenus, icons, disabled items, separators, section headers, full keyboard navigation, automatic viewport flipping, mobile long-press, ARIA roles, and dark mode - all in a single script with no jQuery or framework required.
Live demo and documentation | GitHub repository
Features
- Zero dependencies - one JS file and one CSS file
- Context menu (right-click) or dropdown (click) mode
- Items from a JavaScript array, a function (dynamic), or existing HTML markup
- Nested submenus to any depth, with automatic left flip
- Icons, disabled items, separators, and non-clickable section headers
- Viewport-aware positioning that never spills off screen
- Keyboard navigation (arrow keys, Enter, Space, Escape) and ARIA roles
- Mobile touch long-press support
- Scrollable menus via maxHeight
- Open animation with prefers-reduced-motion support
- Dark mode via prefers-color-scheme
- Auto close on outside click, Escape, scroll, resize, and blur
Installation
Download
<link rel="stylesheet" href="hcg-context-menu.css">
<script src="hcg-context-menu.js"></script>npm
npm install hcg-context-menuconst hcgContextMenu = require("hcg-context-menu");
// or with a bundler / ES modules
import hcgContextMenu from "hcg-context-menu";
import "hcg-context-menu/hcg-context-menu.css";Basic usage
hcgContextMenu({
target: "#my-area",
items: [
{ label: "Cut", icon: "✂", action: function (ctx) { console.log("cut"); } },
{ label: "Copy", icon: "📋", action: function () { console.log("copy"); } },
{ label: "Paste", disabled: true },
{ separator: true },
{ label: "Share", submenu: [
{ label: "Email", action: function () {} },
{ label: "Link", action: function () {} }
] },
{ separator: true },
{ label: "Delete", action: function () {} }
]
});Dropdown mode
hcgContextMenu({
target: "#menu-button",
mode: "dropdown",
items: [
{ label: "Profile", action: function () {} },
{ label: "Settings", action: function () {} },
{ separator: true },
{ label: "Sign out", action: function () {} }
]
});Items from existing HTML
<ul id="my-menu">
<li data-hcg-icon="✎" data-hcg-value="edit">Edit</li>
<li data-hcg-value="copy">Copy</li>
<li class="hcg-context-menu-separator"></li>
<li data-hcg-disabled data-hcg-value="paste">Paste</li>
</ul>hcgContextMenu({
target: "#my-area",
source: "#my-menu",
onSelect: function (item) { console.log(item.value); }
});Markup attribute reference
The source element must be a <ul> (or an element containing one). Each direct <li> becomes a menu item; the attributes/classes on the <li> decide its type. The original list is hidden automatically.
| Attribute / class on <li> | Required | Purpose |
|---|---|---|
| text content | yes, for normal items | Used as the item label. |
| data-hcg-label | optional | Overrides the label text (handy when the <li> has a nested list). |
| data-hcg-html | optional | Trusted HTML for the label, rendered as is (not escaped). |
| data-hcg-icon | optional | Icon / emoji / HTML entity shown before the label. |
| data-hcg-value | optional | Value passed to onSelect so you can identify the item. |
| data-hcg-disabled | optional | Greys out the item and blocks clicks. |
| class="hcg-context-menu-separator" or data-hcg-separator | optional | Divider line (use an empty <li>). |
| class="hcg-context-menu-header" or data-hcg-header | optional | Non-clickable section heading. |
| nested <ul> | optional | Turns the item into a submenu. |
Clickable: normal items and submenu parents. Not clickable: separators, headers, and disabled items.
No attribute is strictly required - a plain <li>Copy</li> is a valid clickable item. Add data-hcg-value when you need to identify it in onSelect.
Note: per-item action functions are only available with the JavaScript array form. With the HTML markup source, handle clicks via the onSelect callback (usually by reading item.value).
Full markup example using every item type:
<ul id="full-menu">
<li class="hcg-context-menu-header">Edit</li>
<li data-hcg-icon="✂" data-hcg-value="cut">Cut</li>
<li data-hcg-icon="📋" data-hcg-value="copy">Copy</li>
<li data-hcg-disabled data-hcg-value="paste">Paste</li>
<li class="hcg-context-menu-separator"></li>
<li data-hcg-icon="🔗" data-hcg-label="Share">Share
<ul>
<li data-hcg-value="email">Email</li>
<li data-hcg-value="link">Link</li>
</ul>
</li>
</ul>hcgContextMenu({
target: "#my-area",
source: "#full-menu",
onSelect: function (item) { console.log("clicked:", item.value); }
});Dynamic items
Pass a function to rebuild the menu on every open, based on what was clicked.
hcgContextMenu({
target: "#board",
items: function (ctx) {
return ctx.target.classList.contains("card")
? [{ header: "Card" }, { label: "Edit" }, { label: "Delete" }]
: [{ label: "New card" }];
}
});Options
| Option | Type | Default | Description |
|---|---|---|---|
| target | String or Element | document | Element that triggers the menu. Selector or DOM node. |
| items | Array or Function | [] | Item objects, or a function (ctx) returning the array. |
| source | String or Element | null | Existing <ul> to build the menu from when no items are given. |
| mode | String | "context" | "context" opens at the pointer on right-click; "dropdown" opens below the element on click (and flips above it when there is no room below). |
| longPress | Boolean | true | Open with a touch long-press on mobile (context mode only). |
| longPressDelay | Number | 500 | Milliseconds to hold before a long-press opens the menu. |
| maxHeight | Number or String | null | Max panel height before it scrolls. Number = px, string used as is. |
| animation | Boolean | true | Fade and scale on open. Disabled under prefers-reduced-motion. |
| className | String | "" | Extra class added to the root element. |
| onOpen | Function | null | Called when the menu opens. Receives context. |
| onSelect | Function | null | Called when an item is selected. Receives item, context. |
| onClose | Function | null | Called when the menu closes. Receives a reason: "select" or "dismiss". |
Item fields
| Field | Type | Description |
|---|---|---|
| label | String | Plain text shown for the item. HTML is escaped, not rendered. |
| html | String | Trusted HTML for the label, rendered as is (not escaped). Use instead of label for formatting; only pass content you control. |
| header | String | Renders a non-clickable section heading. |
| icon | String | Optional icon, emoji, or HTML entity before the label. |
| value | String | Optional value returned in onSelect. |
| action | Function | Run when the item is clicked. |
| disabled | Boolean | Greys out the item and blocks interaction. |
| separator | Boolean | Renders a divider line. |
| submenu | Array | Child items that open as a submenu. |
Instance methods
| Method | Description |
|---|---|
| open(x, y, event) | Opens the menu at the given coordinates. |
| close() | Closes the menu and removes it from the DOM. |
| setItems(items) | Replaces the items at runtime (array or function). |
| destroy() | Closes and unbinds all trigger/touch listeners. Use for SPA cleanup. |
Static helper: hcgContextMenu.closeAll() closes every open menu.
Events
| Event | Fired when | Arguments |
|---|---|---|
| onOpen | The menu opens. | context |
| onSelect | An item is clicked. | item, context |
| onClose | The menu closes. | reason ("select" or "dismiss") |
The context object contains x, y, target (the clicked element), and event.
Handling clicks in one place
Instead of giving every item its own action, you can leave items as plain labels (optionally with a value) and handle all clicks in a single onSelect, then branch on which item was clicked. Branching on value is cleanest since it stays stable even if the label changes.
hcgContextMenu({
target: "#my-area",
onSelect: function (item) {
console.log("selected:", item);
if (item.value === "copy") doCopy();
if (item.value === "paste") doPaste();
if (item.value === "delete") doDelete();
},
items: [
{ label: "Copy", value: "copy" },
{ label: "Paste", value: "paste" },
{ separator: true },
{ label: "Delete", value: "delete" }
]
});A switch reads well with many items, and you can branch on item.label if you prefer not to set values:
onSelect: function (item) {
switch (item.value) {
case "copy": doCopy(); break;
case "delete": doDelete(); break;
}
}This also works with the HTML markup source via data-hcg-value. Per-item action functions and a central onSelect can be combined - the action runs first, then onSelect.
Keyboard
| Key | Action | |---|---| | Arrow Down / Up | Move between items | | Arrow Right | Open submenu | | Arrow Left / Escape | Close submenu or menu | | Enter / Space | Select the focused item |
Browser support
Works in all modern browsers: Chrome, Firefox, Edge, Safari, Opera, and mobile browsers (long-press to open).
License
MIT - HTML Code Generator (https://www.html-code-generator.com/)
