npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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-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/)