Oblivion
Oblivion
Docs
Go back
Component by
OV
Oblivion

Right-click Playground

Right-click anywhere inside this box to see the glass context menu in action.

TSX
glass-context-menu
"use client"

import React, { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Edit3, Copy, ExternalLink, Star, Trash2 } from "lucide-react";

// Replaced with Tailwind classes for seamless dark/light mode support.

interface GlassContextMenuProps {
  children: React.ReactNode;
  items?: Array<{
    icon: React.ReactNode;
    label: string;
    shortcut?: string;
    danger?: boolean;
    onClick?: () => void;
  } | null>;
}

export function GlassContextMenu({ children, items: customItems }: GlassContextMenuProps) {
  const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
  const ref = useRef<HTMLDivElement>(null);

  const defaultItems: Array<{
    icon: React.ReactNode;
    label: string;
    shortcut?: string;
    danger?: boolean;
    onClick?: () => void;
  } | null> = [
    { icon: <Edit3 size={13} />, label: "Edit content", shortcut: "⌘E" },
    { icon: <Copy size={13} />, label: "Copy to clipboard", shortcut: "⌘C" },
    { icon: <ExternalLink size={13} />, label: "Open in viewer", shortcut: "⌘↵" },
    { icon: <Star size={13} />, label: "Add to favorites", shortcut: "⌘D" },
    null,
    { icon: <Trash2 size={13} />, label: "Delete permanently", shortcut: "⌫", danger: true },
  ];

  const items = customItems || defaultItems;

  useEffect(() => {
    const close = () => setMenu(null);
    window.addEventListener("click", close);
    window.addEventListener("scroll", close);
    window.addEventListener("keydown", (e) => e.key === "Escape" && close());
    return () => { 
      window.removeEventListener("click", close); 
      window.removeEventListener("scroll", close);
    };
  }, []);

  const onContext = (e: React.MouseEvent) => {
    e.preventDefault();
    const rect = ref.current?.getBoundingClientRect();
    if (rect) {
      setMenu({ x: e.clientX - rect.left, y: e.clientY - rect.top });
    }
  };

  return (
    <div ref={ref} onContextMenu={onContext} className="relative select-none w-full h-full z-10">
      {children}
      <AnimatePresence>
        {menu && (
          <motion.div
            initial={{ opacity: 0, scale: 0.92 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.92 }}
            transition={{ type: "spring", stiffness: 450, damping: 30 }}
            onClick={(e) => e.stopPropagation()}
            className="absolute z-[99] min-w-[220px] rounded-[14px] p-1.5 bg-white/70 dark:bg-zinc-900/70 backdrop-blur-2xl border border-black/5 dark:border-white/10 shadow-[0_20px_60px_rgba(0,0,0,0.1)] dark:shadow-[0_20px_60px_rgba(0,0,0,0.5)]"
            style={{ top: menu.y, left: menu.x }}
          >
            {items.map((item, i) =>
              item === null ? (
                <div key={`sep-${i}`} className="h-[1px] bg-black/5 dark:bg-white/10 my-1 flex-shrink-0" />
              ) : (
                <motion.div
                  key={item.label}
                  initial={{ opacity: 0, x: -6 }}
                  animate={{ opacity: 1, x: 0 }}
                  transition={{ delay: i * 0.02 }}
                  className={`flex items-center gap-2.5 px-3 py-2.5 rounded-[9px] cursor-pointer text-[13px] transition-colors ${
                    item.danger 
                      ? "text-red-500 hover:bg-red-500/10" 
                      : "text-neutral-700 dark:text-zinc-300 hover:bg-black/5 dark:hover:bg-white/10"
                  }`}
                  onClick={() => {
                    item.onClick?.();
                    setMenu(null);
                  }}
                >
                  <span className={item.danger ? "text-red-500" : "text-neutral-500 dark:text-zinc-400"}>{item.icon}</span>
                  <span className="flex-1 font-medium text-neutral-900 dark:text-zinc-100">{item.label}</span>
                  <span className="text-[11px] text-neutral-400 dark:text-zinc-500 font-mono tracking-tighter">{item.shortcut}</span>
                </motion.div>
              )
            )}
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

export const componentMeta = {
  name: "Glass Context Menu",
  description: "A premium glassmorphism context menu that appears exactly where you right-click.",
  props: {
    items: { type: "array", description: "Array of menu items or null for separators", default: "defaultItems" },
    children: { type: "ReactNode", description: "The trigger area content" }
  }
};

export default function GlassContextMenuPreview() {
  return (
    <div className="flex flex-col items-center justify-center min-h-[500px] w-full relative p-8">

      <GlassContextMenu>
        <div className="w-full max-w-sm h-64 border border-dashed border-black/10 dark:border-white/20 rounded-[2rem] bg-white/70 dark:bg-white/5 backdrop-blur-xl flex flex-col items-center justify-center text-center p-8 transition-colors hover:bg-white dark:hover:bg-white/10 cursor-context-menu shadow-sm">
          <div className="w-16 h-16 rounded-2xl bg-black/5 dark:bg-white/5 border border-black/5 dark:border-white/10 flex items-center justify-center mb-4 text-neutral-500 dark:text-zinc-500">
             <Edit3 size={24} />
          </div>
          <h3 className="text-neutral-900 dark:text-white font-bold mb-2">Right-click Playground</h3>
          <p className="text-xs text-neutral-500 dark:text-zinc-400 max-w-[200px] leading-relaxed font-medium">
            Right-click anywhere inside this box to see the glass context menu in action.
          </p>
        </div>
      </GlassContextMenu>
    </div>
  );
}
Save to favoritesCopy to Figma
React

Glass Context Menu

A premium glassmorphism context menu that appears exactly where you right-click.

Installation

$ npx @cosmoo/oblivion add glass-context-menu
Dependenciesnpm install framer-motion lucide-react

Usage Example

tsx
import GlassContextMenu from "@/components/oblivion/glass-context-menu"

export default function Demo() {
  return (
    <div className="p-10 flex justify-center">
      <GlassContextMenu />
    </div>
  )
}