React
Use context-menu-kit with React
Add a headless, customizable context menu to your React app. You can use the default UI, customize each item, or fully control the menu body.
Installation
pnpm add @context-menu-kit/core @context-menu-kit/reactImport styles
The React package ships with a default CSS file. Import it once in your app.
import '@context-menu-kit/react/styles.css'Live demo
Right click the areas below to try the React package directly in the docs.
Right click different areas
This demo shows default UI, custom menu body, disabled items, and different menus for different trigger areas.
Uses the built-in renderer from @context-menu-kit/react.
Uses renderBody to fully control the menu UI.
Another trigger area with its own items and its own menu UI.
Examples
Each example is interactive. Right click the preview area, then copy the source code into your own React project.
Basic menu
Use the default renderer with a simple list of menu items.
Use the built-in menu renderer.
Source code
import { useState } from "react"
import { ContextMenu, ContextMenuProvider } from "@context-menu-kit/react"
import "@context-menu-kit/react/styles.css"
const items = [
{
id: "copy",
label: "Copy",
onClick: () => {}
},
{
id: "rename",
label: "Rename",
onClick: () => {}
},
{
id: "delete",
label: "Delete",
disabled: true
}
]
export function BasicMenuDemo() {
const [message, setMessage] = useState("Right click the card.")
const menuItems = items.map((item) => ({
...item,
onClick: () => {
setMessage(`${item.label} clicked`)
}
}))
return (
<ContextMenuProvider>
<style>{styles}</style>
<div className="basic-demo">
<ContextMenu items={menuItems}>
<div className="basic-card">
<span>Default UI</span>
<strong>Right click here</strong>
<p>Use the built-in menu renderer.</p>
</div>
</ContextMenu>
<div className="basic-result">{message}</div>
</div>
</ContextMenuProvider>
)
}
const styles = `
.basic-demo {
display: grid;
gap: 14px;
}
.basic-card {
box-sizing: border-box;
min-height: 180px;
display: grid;
align-content: center;
gap: 8px;
border: 1px solid #d9e3f0;
border-radius: 20px;
background: #ffffff;
padding: 24px;
cursor: context-menu;
user-select: none;
}
.basic-card span {
width: fit-content;
border-radius: 999px;
background: #eef3ff;
padding: 6px 10px;
color: #4969e8;
font-size: 12px;
font-weight: 700;
}
.basic-card strong {
color: #172033;
font-size: 24px;
}
.basic-card p {
margin: 0;
color: #52637a;
}
.basic-result {
border-radius: 14px;
background: #eef3ff;
padding: 12px 14px;
color: #172033;
font-size: 14px;
}
`
Custom menu body
Use renderBody to fully control the menu UI.
const menu = createContextMenu() menu.open(event) menu.close()
Source code
import { useState } from "react"
import { ContextMenu, ContextMenuProvider, type ContextMenuItem } from "@context-menu-kit/react"
import "@context-menu-kit/react/styles.css"
interface IMata {
shortcut: string
description?: string
disabled?: boolean
danger?: boolean
}
function MenuBody({ items, close }: { items: ContextMenuItem<IMata>[]; close: () => void }) {
return (
<div className="custom-menu">
{items.map((item) => (
<button
key={item.id}
className={item?.data?.danger ? "custom-menu-item danger" : "custom-menu-item"}
disabled={item.disabled}
onClick={() => {
if (item.disabled) return
item.onClick?.(item)
close()
}}
>
<span>
{item.label}
{item.data ? <small>{item.data.description}</small> : null}
</span>
{item.data ? <kbd>{item.data.shortcut}</kbd> : null}
</button>
))}
</div>
)
}
export function CustomBodyDemo() {
const [message, setMessage] = useState("Right click the editor.")
const items: ContextMenuItem<IMata>[] = [
{
id: "copy",
label: "Copy",
data: {
description: "Copy selected text",
shortcut: "โC"
},
onClick: () => setMessage("Copy clicked")
},
{
id: "paste",
label: "Paste",
data: {
description: "Insert from clipboard",
shortcut: "โV"
},
onClick: () => setMessage("Paste clicked")
},
{
id: "format",
label: "Format document",
data: {
description: "Format current file",
shortcut: "โงโฅF"
},
onClick: () => setMessage("Format document clicked")
},
{
id: "delete",
label: "Delete line",
data: {
description: "Disabled example",
shortcut: "โD",
disabled: true,
danger: true
}
}
]
return (
<ContextMenuProvider>
<style>{styles}</style>
<div className="custom-demo">
<ContextMenu
items={items}
renderBody={({ items, close }) => <MenuBody items={items} close={close} />}
>
<div className="editor-card">
<div className="editor-header">
<span>Editor</span>
<small>index.tsx</small>
</div>
<pre>{`const menu = createContextMenu()
menu.open(event)
menu.close()`}</pre>
</div>
</ContextMenu>
<div className="custom-result">{message}</div>
</div>
</ContextMenuProvider>
)
}
const styles = `
.custom-demo {
display: grid;
gap: 14px;
}
.editor-card {
box-sizing: border-box;
min-height: 220px;
border-radius: 22px;
background: #111827;
padding: 18px;
cursor: context-menu;
user-select: none;
box-shadow: 0 20px 50px rgb(17 24 39 / 0.2);
}
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
color: #dbe7ff;
}
.editor-header span {
font-weight: 700;
}
.editor-header small {
color: #8f9bb0;
}
.editor-card pre {
margin: 0;
border-radius: 16px;
background: rgb(0 0 0 / 0.24);
padding: 18px;
color: #c8d7ee;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
font-size: 14px;
line-height: 1.8;
}
.custom-result {
border-radius: 14px;
background: #eef3ff;
padding: 12px 14px;
color: #172033;
font-size: 14px;
}
.custom-menu {
min-width: 240px;
display: grid;
gap: 4px;
border: 1px solid rgb(255 255 255 / 0.12);
border-radius: 16px;
background: #111827;
padding: 8px;
box-shadow: 0 24px 70px rgb(0 0 0 / 0.28);
}
.custom-menu-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
border: 0;
border-radius: 10px;
background: transparent;
padding: 10px 11px;
color: #f7fbff;
font: inherit;
text-align: left;
cursor: pointer;
}
.custom-menu-item:hover {
background: rgb(255 255 255 / 0.08);
}
.custom-menu-item:disabled {
color: #6f7b90;
cursor: not-allowed;
}
.custom-menu-item.danger:not(:disabled) {
color: #ffb4b4;
}
.custom-menu-item span {
display: grid;
gap: 3px;
}
.custom-menu-item small {
color: #8f9bb0;
font-size: 12px;
}
.custom-menu-item kbd {
border-radius: 7px;
background: rgb(255 255 255 / 0.08);
padding: 3px 6px;
color: #aebbd0;
font-size: 11px;
}
`
Multiple trigger areas
Different areas can have different menu items and different UI.
App.tsx
Button.tsx
styles.css
Source code
import { useState } from "react"
import { ContextMenu, ContextMenuProvider, type ContextMenuItem } from "@context-menu-kit/react"
import "@context-menu-kit/react/styles.css"
function MenuBody({ items, close }: { items: ContextMenuItem[]; close: () => void }) {
return (
<div className="multi-menu">
{items.map((item) => (
<button
key={item.id}
className={"multi-menu-item"}
disabled={item.disabled}
onClick={() => {
if (item.disabled) return
item.onClick?.(item)
close()
}}
>
{item.label}
</button>
))}
</div>
)
}
export function MultiAreaDemo() {
const [message, setMessage] = useState("Right click different areas.")
const fileItems: ContextMenuItem[] = [
{
id: "rename",
label: "Rename file",
onClick: () => setMessage("File area: Rename file")
},
{
id: "duplicate",
label: "Duplicate file",
onClick: () => setMessage("File area: Duplicate file")
},
{
id: "delete",
label: "Delete file",
onClick: () => setMessage("File area: Delete file")
}
]
const canvasItems: ContextMenuItem[] = [
{
id: "bring-front",
label: "Bring to front",
onClick: () => setMessage("Canvas area: Bring to front")
},
{
id: "send-back",
label: "Send to back",
onClick: () => setMessage("Canvas area: Send to back")
},
{
id: "lock",
label: "Lock layer",
disabled: true
}
]
return (
<ContextMenuProvider>
<style>{styles}</style>
<div className="multi-demo">
<div className="multi-grid">
<ContextMenu
items={fileItems}
renderBody={({ items, close }) => (
<MenuBody items={items as ContextMenuItem[]} close={close} />
)}
>
<div className="multi-card file-card">
<span>File area</span>
<strong>Right click files</strong>
<div className="file-list">
<p>App.tsx</p>
<p>Button.tsx</p>
<p>styles.css</p>
</div>
</div>
</ContextMenu>
<ContextMenu
items={canvasItems}
renderBody={({ items, close }) => (
<MenuBody items={items as ContextMenuItem[]} close={close} />
)}
>
<div className="multi-card canvas-card">
<span>Canvas area</span>
<strong>Right click canvas</strong>
<div className="canvas-preview">
<i className="shape one" />
<i className="shape two" />
<i className="shape three" />
</div>
</div>
</ContextMenu>
</div>
<div className="multi-result">{message}</div>
</div>
</ContextMenuProvider>
)
}
const styles = `
.multi-demo {
display: grid;
gap: 14px;
}
.multi-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.multi-card {
box-sizing: border-box;
min-height: 240px;
display: flex;
flex-direction: column;
border: 1px solid #dfe7f2;
border-radius: 22px;
background: #ffffff;
padding: 22px;
cursor: context-menu;
user-select: none;
}
.multi-card span {
width: fit-content;
border-radius: 999px;
background: #eef3ff;
padding: 6px 10px;
color: #4969e8;
font-size: 12px;
font-weight: 700;
}
.multi-card strong {
margin-top: 14px;
color: #172033;
font-size: 22px;
}
.file-list {
display: grid;
gap: 8px;
margin-top: auto;
}
.file-list p {
margin: 0;
border: 1px solid #e3eaf5;
border-radius: 12px;
background: #f8fbff;
padding: 9px 11px;
color: #43546c;
font-size: 13px;
}
.canvas-card {
background:
radial-gradient(circle at top right, rgb(28 184 134 / 0.18), transparent 34%),
#ffffff;
}
.canvas-preview {
position: relative;
min-height: 110px;
margin-top: auto;
border: 1px dashed #b9d8ca;
border-radius: 18px;
background:
linear-gradient(90deg, rgb(23 32 51 / 0.04) 1px, transparent 1px),
linear-gradient(0deg, rgb(23 32 51 / 0.04) 1px, transparent 1px);
background-size: 18px 18px;
}
.shape {
position: absolute;
display: block;
border-radius: 16px;
box-shadow: 0 12px 28px rgb(23 32 51 / 0.14);
}
.shape.one {
width: 76px;
height: 42px;
left: 22px;
top: 20px;
background: #4969e8;
}
.shape.two {
width: 56px;
height: 56px;
right: 42px;
top: 16px;
border-radius: 999px;
background: #20b886;
}
.shape.three {
width: 92px;
height: 28px;
right: 24px;
bottom: 16px;
background: #f59f00;
}
.multi-result {
border-radius: 14px;
background: #eef3ff;
padding: 12px 14px;
color: #172033;
font-size: 14px;
}
.multi-menu {
min-width: 180px;
display: grid;
gap: 6px;
border: 1px solid #d9eee6;
border-radius: 18px;
background: #ffffff;
padding: 10px;
box-shadow: 0 24px 70px rgb(23 32 51 / 0.18);
}
.multi-menu-item {
border: 0;
border-radius: 12px;
background: #f2fbf7;
padding: 10px 12px;
color: #173f31;
font: inherit;
font-size: 14px;
text-align: left;
cursor: pointer;
}
.multi-menu-item:hover {
background: #ddf7ec;
}
.multi-menu-item.danger {
background: #fff0f0;
color: #c92a2a;
}
.multi-menu-item.danger:hover {
background: #ffe3e3;
}
.multi-menu-item:disabled {
color: #9aa8ba;
cursor: not-allowed;
}
@media (max-width: 720px) {
.multi-grid {
grid-template-columns: 1fr;
}
}
`
Basic usage
import {
ContextMenu,
ContextMenuProvider,
type ContextMenuItem,
} from '@context-menu-kit/react'
import '@context-menu-kit/react/styles.css'
const items: ContextMenuItem[] = [
{
id: 'copy',
label: 'Copy',
onSelect: () => {
console.log('Copy')
},
},
{
id: 'paste',
label: 'Paste',
disabled: true,
onSelect: () => {
console.log('Paste')
},
},
]
export function App() {
return (
<ContextMenuProvider>
<ContextMenu items={items}>
<div className="target">
Right click here
</div>
</ContextMenu>
</ContextMenuProvider>
)
}Customize item UI
<ContextMenu
items={items}
renderItem={({ item, close }) => (
<button
disabled={item.disabled}
onClick={() => {
item.onSelect?.()
close()
}}
>
{item.label}
</button>
)}
>
<div>Right click here</div>
</ContextMenu>Customize menu body
<ContextMenu
items={items}
renderBody={({ items, close }) => (
<div className="my-menu">
{items.map((item) => (
<button
key={item.id}
disabled={item.disabled}
onClick={() => {
item.onSelect?.()
close()
}}
>
{item.label}
</button>
))}
</div>
)}
>
<div>Right click here</div>
</ContextMenu>Different areas, different menus
<ContextMenu items={fileItems}>
<div>File area</div>
</ContextMenu>
<ContextMenu items={editorItems} renderBody={renderEditorMenu}>
<div>Editor area</div>
</ContextMenu>
<ContextMenu items={canvasItems} renderItem={renderCanvasItem}>
<div>Canvas area</div>
</ContextMenu>