ray-menuray-menu

Web Component

Using the ray-menu Web Component

Web Component

The <ray-menu> Web Component provides a framework-agnostic way to add radial menus to any web page. It uses an imperative API — items are set via JavaScript, and the menu is opened programmatically.

Import

import "ray-menu";

Or via script tag:

<script type="module" src="ray-menu/dist/wc/ray-menu.mjs"></script>

Basic Usage

<ray-menu id="menu"></ray-menu>

<script type="module">
  import "ray-menu";

  const menu = document.getElementById("menu");

  menu.items = [
    { id: "copy", label: "Copy" },
    { id: "paste", label: "Paste" },
    { id: "cut", label: "Cut" },
    { id: "delete", label: "Delete" },
  ];

  document.addEventListener("contextmenu", (e) => {
    e.preventDefault();
    menu.open(e.clientX, e.clientY);
  });

  menu.addEventListener("ray-select", (e) => {
    console.log("Selected:", e.detail);
  });
</script>

Attributes

AttributeTypeDefaultDescription
radiusnumber120Radius of the menu ring in pixels
inner-radiusnumber40Inner dead zone radius in pixels
center-deadzonenumber30Center deadzone radius for selection
infinite-selectionbooleantrueSectors extend infinitely outward
infinite-thresholdnumber0Max distance for infinite selection (0 = infinite)
edge-behavior"shift" | "flip" | "none""flip"How to handle viewport edge collisions
show-trail-pathbooleanfalseShow drift trace trail path
show-anchor-linebooleanfalseShow anchor line to hovered item
center-transparentbooleantrueMake center area transparent
instant-drag-throughbooleanfalseEnable instant drag-through to submenus
scroll-behavior"close" | "keep" | "lock" | "none""close"Behavior when page scrolls while menu is open
scroll-thresholdnumber10Scroll distance (px) before closing
start-anglenumber-90Start angle in degrees (top = -90)
sweep-anglenumber360Total sweep angle in degrees
variant"slice" | "bubble""slice"Visual variant: arc slices or circle bubbles
staticbooleanfalseUse relative positioning (dock mode)
default-openbooleanfalseOpen automatically on mount (use with static)

Properties

PropertyTypeDescription
itemsMenuItem[]Array of menu items to display
isOpenbooleanWhether the menu is currently open
isDropTargetbooleanWhether in drag-drop target mode
isLoadingbooleanWhether async children are loading
submenuDepthnumberCurrent submenu nesting depth
interface MenuItem {
  id: string;
  label: string;
  icon?: string;
  shortcut?: string;
  disabled?: boolean;
  selectable?: boolean;
  children?: MenuItem[];
  loadChildren?: () => Promise<MenuItem[]>;
  onSelect?: () => void;
}

Methods

MethodDescription
open(x, y)Open the menu at viewport coordinates
close()Close the menu
toggle(x, y)Toggle menu open/closed
goBack()Navigate back one submenu level
goToRoot()Navigate back to root menu
openAsDropTarget(x, y)Open in drag-drop target mode
updateHoverFromPoint(x, y)Update hover state from external coordinates
dropOnHovered(data?)Complete drop on currently hovered item
cancelDrop()Cancel drag-drop operation
getHoveredItem()Get the currently hovered MenuItem

Events

ray-select

Fired when an item is selected. event.detail is the selected MenuItem.

menu.addEventListener("ray-select", (e) => {
  console.log("Selected:", e.detail.label);
});

ray-open

Fired when the menu opens. event.detail is { x, y } position.

menu.addEventListener("ray-open", (e) => {
  console.log("Opened at:", e.detail.x, e.detail.y);
});

ray-close

Fired when the menu closes.

menu.addEventListener("ray-close", () => {
  console.log("Menu closed");
});

ray-submenu-enter

Fired when entering a submenu. event.detail is { item, depth }.

ray-submenu-exit

Fired when exiting a submenu. event.detail is { item, depth }.

ray-drop

Fired on drop in drag-drop mode. event.detail is { item, data }.

ray-load-start / ray-load-complete / ray-load-error

Fired during async child loading.

Submenus are defined via the children property on menu items:

menu.items = [
  { id: "copy", label: "Copy" },
  {
    id: "more",
    label: "More",
    children: [
      { id: "option1", label: "Option 1" },
      { id: "option2", label: "Option 2" },
    ],
  },
];

Async Submenus

Load submenu items on demand with loadChildren:

menu.items = [
  {
    id: "files",
    label: "Files",
    loadChildren: async () => {
      const files = await fetchFiles();
      return files.map((f) => ({ id: f.id, label: f.name }));
    },
  },
];

Variants

Slice (default)

Items render as arc/pie slices filling the ring. This is the classic radial menu look.

Bubble

Items render as circles arranged in a ring. Set variant="bubble" to enable:

<ray-menu variant="bubble"></ray-menu>

The bubble variant works best with items that have an icon property — the icon displays large inside the bubble with the label text below it.

menu.items = [
  { id: "copy", label: "Copy", icon: "📋" },
  { id: "paste", label: "Paste", icon: "📌" },
  { id: "delete", label: "Delete", icon: "🗑️" },
];

Bubble Submenus

Unlike the slice variant where submenus replace the ring concentrically, bubble submenus branch outward from the parent bubble as a fan of child bubbles. The root ring stays visible and dimmed, with a connector line linking the parent to its submenu cluster.

menu.items = [
  {
    id: "format",
    label: "Format",
    icon: "🎨",
    children: [
      { id: "bold", label: "Bold", icon: "B" },
      { id: "italic", label: "Italic", icon: "I" },
      { id: "underline", label: "Underline", icon: "U" },
    ],
  },
  { id: "copy", label: "Copy", icon: "📋" },
  { id: "paste", label: "Paste", icon: "📌" },
];

Behavior:

  • Root bubbles stay visible but dimmed when a submenu opens
  • The selected parent bubble is highlighted
  • Child bubbles fan outward from the parent in a radial arc
  • A dashed connector line links the parent to the submenu cluster
  • Hover detection uses infinite selection on the active submenu fan
  • Moving the pointer back into the root ring band exits the submenu
  • Multi-level submenus are supported (each level branches from its parent)
  • Deeper submenu levels use progressively smaller radii to prevent sprawl
  • Submenu fans automatically adjust their angle when near viewport edges

Bubble-specific CSS variables:

VariableDefaultDescription
--ray-bubble-fillrgba(50, 50, 60, 0.7)Bubble background
--ray-bubble-fill-hoverrgba(100, 180, 255, 0.5)Bubble hover fill
--ray-bubble-strokergba(255, 255, 255, 0.15)Bubble border
--ray-bubble-stroke-hoverrgba(100, 180, 255, 0.8)Bubble hover border

Theming

Customize appearance with CSS custom properties on the <ray-menu> element:

ray-menu {
  --ray-bg: #1a1a2e;
  --ray-text: #ffffff;
  --ray-accent: #ff6b6b;
  --ray-accent-text: white;
  --ray-border: rgba(255, 255, 255, 0.15);
  --ray-arc-fill: rgba(50, 50, 60, 0.6);
  --ray-arc-fill-hover: rgba(255, 107, 107, 0.4);
  --ray-arc-stroke: rgba(255, 255, 255, 0.1);
  --ray-arc-stroke-hover: rgba(255, 107, 107, 0.7);
  --ray-radius: 8px;
  --ray-font-size: 14px;
  --ray-transition: 150ms ease;
}

Keyboard Navigation

When the menu is open:

KeyAction
ArrowLeft/RightMove focus between items
ArrowDown/Enter/SpaceSelect focused item or enter submenu
ArrowUpGo back to parent or close
BackspaceGo back to parent submenu
EscapeClose the menu
Home/EndJump to first/last item
1-9Quick select item by number

On this page