Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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 Β |Β  npm package

npm install hcg-context-menu

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-menu
const 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="&#9998;" 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="&#9986;"   data-hcg-value="cut">Cut</li>
  <li data-hcg-icon="&#128203;" 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="&#128279;" 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/)

About

Lightweight dependency-free JavaScript context menu and dropdown library - submenus, icons, headers, keyboard navigation, mobile long-press, viewport flipping, and dark mode. No jQuery.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages