Docs
Go back
Component by
OV
OblivionRight-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>
);
}"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-menuDependencies
npm install framer-motion lucide-reactUsage Example
tsx
import GlassContextMenu from "@/components/oblivion/glass-context-menu"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<GlassContextMenu />
</div>
)
}import GlassContextMenu from "@/components/oblivion/glass-context-menu"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<GlassContextMenu />
</div>
)
}