Overlays
Context Menu
Right-click context menu built on the shared Menu overlay and styled with the quebi design system. Captures the pointer position to anchor the popover, reusing the dark surface, cyan hairlines, and dropdown item styling with sections, separators, shortcuts, and intents.
overlaymenucontext-menuright-clickactions
Right-click target
Right-click the surface to open the menu anchored at the pointer.
Icons and shortcuts
Items can carry leading icons and trailing keyboard shortcut hints.
Header and sections
Group related items under labelled sections with a leading header.
Source
Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import { createContext, use, useRef, useState } from "react"
import {
MenuContent,
type MenuContentProps,
MenuDescription,
MenuHeader,
MenuItem,
MenuLabel,
MenuSection,
MenuSeparator,
MenuShortcut,
} from "@/components/menu"
import { cn } from "@/lib/utils"
/**
* ContextMenu — quebi design system
*
* A right-click (context) menu built on the shared Menu overlay. The trigger
* captures the pointer position on `contextmenu` and anchors the popover there,
* reusing the dark quebi-bg surface, cyan hairlines, and dropdown item styling.
*
* Restyled from the original onto quebi tokens — only the trigger's own classes
* changed; all menu styling is inherited from `@/components/menu`.
*/
interface ContextMenuTriggerContextType {
buttonRef: React.RefObject<HTMLButtonElement | null>
contextMenuOffset: { offset: number; crossOffset: number } | null
setContextMenuOffset: React.Dispatch<
React.SetStateAction<{ offset: number; crossOffset: number } | null>
>
}
const ContextMenuTriggerContext = createContext<ContextMenuTriggerContextType | undefined>(
undefined,
)
const useContextMenuTrigger = () => {
const context = use(ContextMenuTriggerContext)
if (!context) {
throw new Error("useContextMenuTrigger must be used within a ContextMenuTrigger")
}
return context
}
interface ContextMenuProps {
children: React.ReactNode
}
const ContextMenu = ({ children }: ContextMenuProps) => {
const [contextMenuOffset, setContextMenuOffset] = useState<{
offset: number
crossOffset: number
} | null>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
return (
<ContextMenuTriggerContext.Provider
value={{ buttonRef, contextMenuOffset, setContextMenuOffset }}
>
{children}
</ContextMenuTriggerContext.Provider>
)
}
type ContextMenuTriggerProps = React.ButtonHTMLAttributes<HTMLButtonElement>
const ContextMenuTrigger = ({ className, ...props }: ContextMenuTriggerProps) => {
const { buttonRef, setContextMenuOffset } = useContextMenuTrigger()
const onContextMenu = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
const rect = e.currentTarget.getBoundingClientRect()
setContextMenuOffset({
offset: e.clientY - rect.bottom,
crossOffset: e.clientX - rect.left,
})
}
return (
<button
className={cn(
"cursor-default outline-hidden disabled:opacity-60 disabled:forced-colors:disabled:text-[GrayText]",
"focus-visible:ring-2 focus-visible:ring-quebi-brand/50 focus-visible:ring-offset-2 focus-visible:ring-offset-quebi-bg",
className,
)}
ref={buttonRef}
aria-haspopup="menu"
onContextMenu={onContextMenu}
{...props}
/>
)
}
type ContextMenuContentProps<T> = Omit<
MenuContentProps<T>,
"arrow" | "isOpen" | "onOpenChange" | "triggerRef" | "placement" | "shouldFlip"
>
const ContextMenuContent = <T extends object>(props: ContextMenuContentProps<T>) => {
const { contextMenuOffset, setContextMenuOffset, buttonRef } = useContextMenuTrigger()
return contextMenuOffset ? (
<MenuContent
popover={{
isOpen: !!contextMenuOffset,
shouldFlip: false,
triggerRef: buttonRef,
onOpenChange: () => setContextMenuOffset(null),
placement: "bottom left",
offset: contextMenuOffset.offset,
crossOffset: contextMenuOffset.crossOffset,
}}
onClose={() => setContextMenuOffset(null)}
{...props}
/>
) : null
}
const ContextMenuItem = MenuItem
const ContextMenuSeparator = MenuSeparator
const ContextMenuDescription = MenuDescription
const ContextMenuSection = MenuSection
const ContextMenuHeader = MenuHeader
const ContextMenuShortcut = MenuShortcut
const ContextMenuLabel = MenuLabel
export type { ContextMenuProps }
export {
ContextMenu,
ContextMenuContent,
ContextMenuDescription,
ContextMenuHeader,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSection,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
}