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
| Attribute | Type | Default | Description |
|---|---|---|---|
radius | number | 120 | Radius of the menu ring in pixels |
inner-radius | number | 40 | Inner dead zone radius in pixels |
center-deadzone | number | 30 | Center deadzone radius for selection |
infinite-selection | boolean | true | Sectors extend infinitely outward |
infinite-threshold | number | 0 | Max distance for infinite selection (0 = infinite) |
edge-behavior | "shift" | "flip" | "none" | "flip" | How to handle viewport edge collisions |
show-trail-path | boolean | false | Show drift trace trail path |
show-anchor-line | boolean | false | Show anchor line to hovered item |
center-transparent | boolean | true | Make center area transparent |
instant-drag-through | boolean | false | Enable instant drag-through to submenus |
scroll-behavior | "close" | "keep" | "lock" | "none" | "close" | Behavior when page scrolls while menu is open |
scroll-threshold | number | 10 | Scroll distance (px) before closing |
start-angle | number | -90 | Start angle in degrees (top = -90) |
sweep-angle | number | 360 | Total sweep angle in degrees |
variant | "slice" | "bubble" | "slice" | Visual variant: arc slices or circle bubbles |
static | boolean | false | Use relative positioning (dock mode) |
default-open | boolean | false | Open automatically on mount (use with static) |
Properties
| Property | Type | Description |
|---|---|---|
items | MenuItem[] | Array of menu items to display |
isOpen | boolean | Whether the menu is currently open |
isDropTarget | boolean | Whether in drag-drop target mode |
isLoading | boolean | Whether async children are loading |
submenuDepth | number | Current submenu nesting depth |
MenuItem
interface MenuItem {
id: string;
label: string;
icon?: string;
shortcut?: string;
disabled?: boolean;
selectable?: boolean;
children?: MenuItem[];
loadChildren?: () => Promise<MenuItem[]>;
onSelect?: () => void;
}Methods
| Method | Description |
|---|---|
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
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:
| Variable | Default | Description |
|---|---|---|
--ray-bubble-fill | rgba(50, 50, 60, 0.7) | Bubble background |
--ray-bubble-fill-hover | rgba(100, 180, 255, 0.5) | Bubble hover fill |
--ray-bubble-stroke | rgba(255, 255, 255, 0.15) | Bubble border |
--ray-bubble-stroke-hover | rgba(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:
| Key | Action |
|---|---|
ArrowLeft/Right | Move focus between items |
ArrowDown/Enter/Space | Select focused item or enter submenu |
ArrowUp | Go back to parent or close |
Backspace | Go back to parent submenu |
Escape | Close the menu |
Home/End | Jump to first/last item |
1-9 | Quick select item by number |