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

@get-set/gs-contextmenu

v1.0.2

Published

Get-Set Context Menu

Readme

GSContextMenu

A dependency-free, feature-rich context menu / dropdown menu available in two flavours from one codebase:

  • Native / vanilla JS — a window.GSContextMenu(selector, params) factory plus a window.GSContextMenuConfigue instance registry. jQuery and HTMLElement.prototype adapters are wired automatically.
  • React — a <GSContextMenu /> component with an imperative ref handle.

Both share the exact same engine (actions/, constants/, helpers/, types/), so behaviour is identical across the two.

Features

  • Four ways to use it — vanilla window.GSContextMenu, jQuery $(...).GSContextMenu(), element.GSContextMenu(), and the React <GSContextMenu> component.
  • Trigger modescontextmenu (right-click, default), click (left-click dropdown), both, or manual (imperative API only).
  • Cursor-anchored positioning — opens toward the bottom-right of the pointer and flips up / left when it would overflow the bottom / right edge, then shifts into view. Anchored (dropdown) mode uses a full placement resolver with auto-flip + shift.
  • Nested submenus — static arrays or dynamic children(ctx) functions, opened on hover (with configurable open/close delays) or by keyboard.
  • Rich item typesitem, checkbox, radio (with sibling grouping), separator (or the '-' shorthand), and section headers.
  • Item extras — leading icon, right-aligned shortcut hint, danger styling, disabled, href/target links, keepOpen, and per-item className.
  • Full keyboard support — Arrow navigation, ArrowRight/ArrowLeft to open/close submenus (RTL-aware), Home/End, Enter/Space to activate, Escape (closes the deepest level first), Tab to dismiss, and type-ahead.
  • Dismissal controls — close on outside click, Escape, window blur, scroll, or after selecting.
  • Animations — 9 named entrance/exit animations (fade, scale/zoom, four slide-*, flip, perspective) + none, with tunable duration/easing and prefers-reduced-motion support.
  • Theminglight / dark / auto (follows OS), a custom accentColor token, size and density presets, elevation, corner radius, min/max width, max height (scrolling list), and an optional dim / blur backdrop.
  • RTL — right-to-left layout with inverted submenu / flip preferences.
  • Accessibilityrole="menu" with menuitem / menuitemcheckbox / menuitemradio / separator roles, aria-haspopup / aria-expanded / aria-checked / aria-disabled, roving focus, and focus restore on close.
  • React-only gsx prop — scoped, per-instance inline styles injected into <head> and cleaned up on unmount.

Installation

npm i @get-set/gs-contextmenu

Compatibility

| Target | Requirement | |---|---| | React (the <GSContextMenu> component) | React 16.8+ (Hooks are required), and 17 / 18 / 19. React is an optional peer dependency — you only need it for the component. | | Native / vanilla (window.GSContextMenu) | No framework. Any modern evergreen browser. | | jQuery | Optional — the $.fn.GSContextMenu adapter is registered only when window.jQuery is present when the bundle loads. | | TypeScript | First-class — type declarations (.d.ts) ship in the package. | | SSR / Next.js | Safe to import server-side (no DOM access at module load). Render the component inside a Client Component ('use client'). |

The peer range is ^16.8.0 || ^17.0 || ^18.0 || ^19.0, with react / react-dom marked optional so the native build has zero peer dependencies.

Package entry points

From package.json:

| Field | Value | |---|---| | name | @get-set/gs-contextmenu | | main | dist/components/GSContextMenu.js | | module | dist/components/GSContextMenu.js | | types | dist/components/GSContextMenu.d.ts | | exports['.'].import | ./dist/components/GSContextMenu.js (React/ESM) | | exports['.'].require | ./dist-js/bundle.js (native window-global bundle) | | files | dist, dist-js, styles, example.html |

Project layout

GSContextMenu.ts             # native entry (webpack -> dist-js/bundle.js, window global + adapters)
components/GSContextMenu.tsx # React component (tsc -> dist/, npm entry)
actions/                     # shared engine (init/open/close/refresh/destroy/getCurrentParams/animate)
constants/                   # animations, placements, defaultParams
helpers/                     # items, keyboard, layout, position, render, uihelpers
types/                       # Params / Ref / Window augmentation
components/styles/           # SCSS + compiled CSS + CSS-as-TS (runtime injection for React)
styles/                      # SCSS + compiled CSS (for <link> use by the native build)

Build

npm install
npm run build          # builds both targets
npm run build:js       # native bundle -> dist-js/bundle.js
npm run build:react    # React + types  -> dist/

Tests

Unit tests use Vitest + jsdom:

npm test         # run once
npm run test:watch

Coverage spans the pure logic (param resolution, item normalization, placement math, positioning flip/clamp, animation resolution), the native DOM target (factory, registry, right-click open, item/checkbox/radio activation, submenus, dismissal, theming), and the React component (rendering, imperative ref, activation, submenus, keyboard nav + type-ahead, prop-stripping, gsx inject/cleanup, registry register/unregister). Note: jsdom has no layout engine, so pixel-level positioning is validated via the math helpers rather than rendered geometry.


Usage — 1. Native / vanilla JS

Link the stylesheet, load the bundle, then call the factory with a selector or an element:

<link rel="stylesheet" href="node_modules/@get-set/gs-contextmenu/styles/GSContextMenu.css" />
<script src="node_modules/@get-set/gs-contextmenu/dist-js/bundle.js"></script>

<div id="area">Right-click me</div>

<script>
  window.GSContextMenu('#area', {
    reference: 'area-menu',
    theme: 'dark',
    items: [
      { label: 'Cut', shortcut: 'Ctrl+X', onClick: (item, ctx) => console.log('cut', ctx.target) },
      { label: 'Copy', shortcut: 'Ctrl+C' },
      '-',
      {
        label: 'Share',
        children: [{ label: 'Email' }, { label: 'Messages' }]
      },
      { header: 'View' },
      { label: 'Word wrap', type: 'checkbox', checked: true, keepOpen: true },
      { label: 'Delete', danger: true }
    ]
  });
</script>

The factory accepts either a CSS selector string or a raw HTMLElement (the jQuery + prototype adapters both pass a raw element here). It returns the instance Ref, or undefined if the element was not found or the reference is already taken.

The instance registry — window.GSContextMenuConfigue

Every instance registers itself under its reference (a supplied string or an auto-generated UUID). Look one up later without keeping the return value around:

const menu = window.GSContextMenuConfigue.instance('area-menu');
menu.openMenu(200, 120);   // open at viewport coords
menu.setItems([{ label: 'Fresh' }]);
menu.closeMenu();
menu.destroy();            // tears down + unregisters

Usage — 2. jQuery

When window.jQuery is present as the bundle loads, a $.fn.GSContextMenu adapter is registered. It constructs one instance per matched element:

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="node_modules/@get-set/gs-contextmenu/dist-js/bundle.js"></script>

<script>
  $('.has-menu').GSContextMenu({
    trigger: 'contextmenu',
    items: [{ label: 'Edit' }, { label: 'Remove', danger: true }]
  });
</script>

Usage — 3. HTMLElement.prototype

The bundle also adds a GSContextMenu method to HTMLElement.prototype, so any element can bind a menu to itself:

document.getElementById('area').GSContextMenu({
  items: [{ label: 'Rename' }, { label: 'Duplicate' }]
});

Usage — 4. React <GSContextMenu>

Wrap the trigger area in <GSContextMenu> and pass the items array. The menu portals into document.body while open:

'use client';
import { useRef } from 'react';
import GSContextMenu, { GSContextMenuHandle } from '@get-set/gs-contextmenu';

export default function Demo() {
  const ref = useRef<GSContextMenuHandle>(null);

  return (
    <GSContextMenu
      ref={ref}
      theme="dark"
      items={[
        { label: 'Cut', shortcut: 'Ctrl+X', onClick: (item, ctx) => console.log(item, ctx) },
        { label: 'Copy', shortcut: 'Ctrl+C' },
        '-',
        { label: 'Share', children: [{ label: 'Email' }, { label: 'Messages' }] },
        { header: 'View' },
        { label: 'Word wrap', type: 'checkbox', checked: true, keepOpen: true },
        { label: 'Delete', danger: true }
      ]}
      onSelect={(item) => console.log('selected', item.label)}>
      <div className="canvas">Right-click me</div>
    </GSContextMenu>
  );
}

The React target imports its CSS itself (injected once per document), so no <link> is required.

Controlled vs. uncontrolled

  • Uncontrolled (default): the component owns its open state. Drive it with the imperative ref (ref.current.open(x, y)) or the trigger. Use defaultOpen to start open.
  • Controlled: pass open (boolean) plus onOpenChange(next) and own the state yourself.

The gsx prop (React only)

gsx accepts a (nestable) CSS map that is scoped to the instance via a data-key attribute, injected into <head> on mount and removed on unmount:

<GSContextMenu
  gsx={{ '.gs-contextmenu': { borderRadius: '12px', minWidth: '240px' } }}
  items={items}>
  <div>Right-click me</div>
</GSContextMenu>

Options / Props

These apply to both targets. In native they are keys on the second params argument; in React they are props on <GSContextMenu>. Defaults come from constants/defaultParams.ts.

| Option | Type | Default | Description | |---|---|---|---| | reference | string | auto UUID | Stable id used for the registry + gsx scoping. | | items | GSMenuEntry[] | [] | The menu entries (see the schema below). | | trigger | 'contextmenu' \| 'click' \| 'both' \| 'manual' | 'contextmenu' | How the menu opens. manual wires no listeners (API only). | | preventDefault | boolean | true | Prevent the native browser context menu on contextmenu triggers. | | placement | GSContextMenuPlacement | 'bottom-start' | Anchored placement in click/dropdown mode. | | offset | { x?: number; y?: number } | | Custom nudge from the resolved position. | | gap | number | 4 | Distance (px) between anchor and menu in anchored mode. | | flip | boolean | true | Auto-flip to the opposite side on viewport collision (anchored mode). | | shift | boolean | true | Shift the menu along the cross axis to stay on screen. | | boundaryPadding | number | 8 | Viewport padding kept when flipping / shifting (px). | | submenuOpenDelay | number | 120 | Delay (ms) before a hovered submenu opens. | | submenuCloseDelay | number | 200 | Delay (ms) before a submenu closes on mouse-out. | | closeOnOutside | boolean | true | Close on outside pointerdown. | | closeOnEsc | boolean | true | Close on Escape. | | closeOnBlur | boolean | true | Close when the window loses focus. | | closeOnScroll | boolean | false | Close when the page scrolls. | | closeOnSelect | boolean | true | Close after selecting an item (unless the item sets keepOpen). | | animation | GSContextMenuAnimation \| false | 'scale' | Entrance animation. false disables motion. | | exitAnimation | GSContextMenuAnimation \| false | mirrors animation | Separate exit animation. | | animationDuration | number \| { enter?: number; exit?: number } | 160 | Animation duration in ms. | | animationEasing | string | cubic-bezier(.16,1,.3,1) | CSS easing for the animation. | | reducedMotion | 'auto' \| boolean | 'auto' | Honor prefers-reduced-motion. | | theme | 'light' \| 'dark' \| 'auto' | 'auto' | Visual theme. auto follows prefers-color-scheme. | | accentColor | string | | Accent color (CSS var --gscm-accent) for highlight / focus. | | rtl | boolean | false | Right-to-left layout. | | size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset. | | density | 'comfortable' \| 'compact' | 'comfortable' | Item density. | | minWidth | number \| string | 200 | Minimum menu width (number = px). | | maxWidth | number \| string | | Maximum menu width. | | maxHeight | number \| string | | Max menu height; the list scrolls past this. | | rounded | number \| string | | Corner radius. | | elevation | number | 3 | Shadow elevation preset (0–5). | | backdrop | 'none' \| 'dim' \| 'blur' | 'none' | Full-screen backdrop behind the menu. | | className | string | | Extra class name on the menu root. | | onOpen | () => void | | Fired after the menu opens. | | onClose | () => void | | Fired after the menu closes. | | onSelect | (item) => void | | Fired when an item is selected (activated). | | onItemClick | (item, event) => void | | Fired when an item is clicked, before close. |

React-only props

| Prop | Type | Default | Description | |---|---|---|---| | open | boolean | | Controlled open state. Pair with onOpenChange. | | defaultOpen | boolean | false | Uncontrolled initial open state. | | onOpenChange | (open: boolean) => void | | Fired when the open state should change (controlled pattern). | | gsx | NestedCSS | | Scoped inline styles injected for this instance. | | children | ReactNode | | The trigger area. |

placement accepts any of: top, top-start, top-end, bottom, bottom-start, bottom-end, left, left-start, left-end, right, right-start, right-end. animation accepts: fade, scale, zoom (alias of scale), slide-up, slide-down, slide-left, slide-right, flip, perspective, none.

The GSMenuItem schema

Each entry in items is either the '-' separator shorthand or a GSMenuItem object:

| Field | Type | Description | |---|---|---| | id | string | Stable id (radio grouping fallback + keys). | | label | string | Visible label. | | icon | string \| ReactNode | Leading icon — an HTML string (native) or a ReactNode (React). | | shortcut | string | Right-aligned shortcut hint, e.g. 'Ctrl+C'. | | disabled | boolean | Non-interactive, dimmed. | | danger | boolean | Destructive (red) styling. | | type | 'item' \| 'checkbox' \| 'radio' \| 'separator' \| 'header' | Entry kind. Default 'item'. | | checked | boolean | Checkbox / radio checked state. | | name | string | Radio group name (siblings sharing it uncheck on select). | | header | string | Non-interactive header label (alias for { type: 'header', label }). | | separator | boolean | Divider shorthand ({ separator: true }). | | href | string | Anchor href — renders the item as a link. | | target | string | Anchor target (with href). | | onClick | (item, ctx) => void | Click handler. | | action | (item, ctx) => void | Alias of onClick. | | children | GSMenuItem[] \| ((ctx) => GSMenuItem[]) | Submenu entries (static array or dynamic function). | | items | GSMenuItem[] \| ((ctx) => GSMenuItem[]) | Alias of children. | | keepOpen | boolean | Keep the menu open after activating this item. | | className | string | Extra class name on the item element. |

The ctx (GSMenuContext) passed to onClick / dynamic children is:

| Field | Type | Description | |---|---|---| | target | HTMLElement \| null | The element the menu is bound to (the trigger area). | | x / y | number | The pointer coordinates the menu opened at (viewport space). | | close | () => void | Close the whole menu programmatically. |

Shorthands: '-' and { separator: true } both normalize to a separator; { header: 'Section' } normalizes to a header.

Imperative API / ref methods

Native — methods on the returned Ref (also available via window.GSContextMenuConfigue.instance(ref)):

| Method | Description | |---|---| | openMenu(x?, y?) / open_(x?, y?) | Open at explicit viewport coordinates (default 0, 0). | | openAt(el) | Open anchored to an element (uses placement). | | closeMenu() | Close the menu. | | toggle() | Toggle the open state (re-opens at the last coordinates). | | isOpen() | Whether the menu is currently open. | | setItems(items) | Replace the item list (refreshes if open). | | refresh() | Re-render the open menu. | | destroy() | Tear down + unregister from the registry. |

React — the GSContextMenuHandle exposed via ref:

| Method | Description | |---|---| | open(x?, y?) | Open at explicit viewport coordinates (default 0, 0). | | openAt(el) | Open anchored to an element. | | close() | Close the menu. | | toggle() | Toggle the open state. | | isOpen() | Whether the menu is currently open. | | setItems(items) | Replace the item list. | | refresh() | Re-measure + reposition the open menu. | | destroy() | Tear down (close) the menu. |

Events / callbacks

| Callback | Fires | |---|---| | onOpen() | After the menu opens. | | onClose() | After the menu closes (native: after the exit animation). | | onSelect(item) | When an item is selected (activated). | | onItemClick(item, event) | When an item is clicked, before close. | | onOpenChange(open) | (React, controlled only) When the open state should change. | | item onClick(item, ctx) / action(item, ctx) | When that specific item is activated. |

Keyboard shortcuts

Available while the menu is open (LTR; ArrowLeft / ArrowRight swap under rtl):

| Key | Action | |---|---| | ArrowDown / ArrowUp | Move the highlight within the current level (skips separators / headers / disabled, wraps). | | Home / End | Highlight the first / last interactive item. | | ArrowRight | Open the highlighted item's submenu and focus its first item. | | ArrowLeft | Close the current submenu, returning to the parent level. | | Enter / Space | Activate the highlighted item (or open its submenu). | | Escape | Close the deepest open submenu first; at the root level, close the whole menu. | | Tab | Dismiss the menu. | | type a letter | Type-ahead — jump to the next item whose label starts with the typed buffer (buffer clears after 500 ms). |

Accessibility

  • The menu root is role="menu" with tabindex="-1"; items use role="menuitem", role="menuitemcheckbox", role="menuitemradio", or role="separator"; headers are role="presentation".
  • Submenu parents expose aria-haspopup="menu" and aria-expanded; checkbox / radio items expose aria-checked; disabled items expose aria-disabled.
  • Focus moves into the menu on open and is restored to the previously focused element (usually the trigger) on close.
  • The roving highlight (arrow / type-ahead navigation) is conveyed to assistive technology: the native target moves real DOM focus onto the active item element, while the React target keeps focus on the menu container and points its aria-activedescendant at the active item's id (each menuitem carries a stable id). With an open submenu, focus follows to the deepest menu container so its active descendant is the one announced.
  • Motion honors prefers-reduced-motion when reducedMotion is 'auto' (the default).

Theming & RTL

  • Themestheme="light" | "dark" | "auto" add a gs-contextmenu-theme-* class; auto follows prefers-color-scheme.
  • AccentaccentColor sets the --gscm-accent CSS variable used for the active/focus highlight.
  • Sizingsize (sm/md/lg) and density (comfortable/compact) are class presets; minWidth / maxWidth / maxHeight / rounded become --gscm-min-width / --gscm-max-width / --gscm-max-height / --gscm-radius variables; elevation (0–5) adds a gs-contextmenu-elev-* class.
  • Backdropbackdrop="dim" | "blur" renders a full-screen layer behind the menu.
  • RTLrtl adds gs-contextmenu-rtl and inverts submenu / cursor-flip preferences (submenus open leftward, the root flips left by default).
  • Custom styles — target the public classes (.gs-contextmenu, .gs-contextmenu-item, .gs-contextmenu-item-active, .gs-contextmenu-separator, .gs-contextmenu-header, .gs-contextmenu-item-shortcut, .gs-contextmenu-item-danger, …) in your own CSS, or (React) via the scoped gsx prop.

Examples

Native — dropdown with a dynamic submenu

window.GSContextMenu('#menuBtn', {
  trigger: 'click',
  placement: 'bottom-start',
  theme: 'dark',
  items: [
    { label: 'Profile' },
    { label: 'Recent', children: (ctx) => loadRecent(ctx.target).map((r) => ({ label: r })) },
    '-',
    { label: 'Sign out', danger: true }
  ]
});

React — checkbox + radio group, kept open

<GSContextMenu
  closeOnSelect={false}
  items={[
    { header: 'View' },
    { label: 'Word wrap', type: 'checkbox', checked: true, keepOpen: true },
    '-',
    { header: 'Font size' },
    { label: 'Small', type: 'radio', name: 'size', keepOpen: true },
    { label: 'Medium', type: 'radio', name: 'size', checked: true, keepOpen: true },
    { label: 'Large', type: 'radio', name: 'size', keepOpen: true }
  ]}>
  <div className="editor">Right-click me</div>
</GSContextMenu>

A complete, runnable native demo lives in example.html — open it after npm run build.

License

ISC © Get-Set