Kontextmenü
Ein Rechtsklick-Menü für Dateizeilen, Arbeitsflächen und Datentabellen – kontextbezogene Aktionen im neobrutalistischen Stil mit Rahmen und Schatten.
import {
ContextMenu,
ContextMenuCheckboxItem,Das Kontextmenü öffnet per Rechtsklick (oder langem Drücken auf Touchgeräten) eine schwebende Aktionsliste am Zeiger, mit Unterstützung für Untermenüs, Checkbox- und Radio-Einträge sowie Tastenkürzel. Es basiert auf der Base UI Context Menu-Komponente und folgt dem neobrutalistischen Rezept: dicke Rahmen, harte Schatten und kräftige Typografie.
Die richtige Wahl für:
- Datei- und Zeilenaktionen – Umbenennen, Duplizieren, Löschen auf Dateimanager-Einträgen oder Tabellenzeilen, ohne jede Zeile mit Buttons vollzustellen.
- Canvas-Oberflächen – Whiteboards, Design-Tools und Editoren, in denen die verfügbaren Aktionen davon abhängen, was angeklickt wurde.
- Datengrids – Operationen pro Zelle wie Wert kopieren, danach filtern oder Spalte anpinnen, nah an den Daten, auf die sie wirken.
Installation#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/context-menu.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/context-menu.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/context-menu.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/context-menu.json
Installiere die folgenden Abhängigkeiten:
pnpm add @base-ui/reactnpm install @base-ui/reactyarn add @base-ui/reactbun add @base-ui/react
Kopiere den folgenden Code und füge ihn in dein Projekt ein.
"use client"
import * as React from "react"
import { CheckIcon, ChevronRightIcon } from "lucide-react"
import { ContextMenu as ContextMenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger
data-slot="context-menu-trigger"
className={cn("select-none", className)}
{...props}
/>
)
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn("z-50 max-h-(--radix-context-menu-content-available-height) min-w-36 origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded border-2 bg-popover p-1 text-popover-foreground shadow-md duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/context-menu-item relative flex cursor-default items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 focus:*:[svg]:text-accent-foreground data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="cn-rtl-flip ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn("z-50 min-w-32 origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded border-2 bg-popover p-1 text-popover-foreground shadow-md duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute right-2">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioItem({
className,
children,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute right-2">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
className
)}
{...props}
/>
)
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
Passe die Importpfade an deine Projektkonfiguration an.
Verwendung#
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu"<ContextMenu>
<ContextMenuTrigger>Right click here</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Profile</ContextMenuItem>
<ContextMenuItem>Billing</ContextMenuItem>
<ContextMenuItem>Team</ContextMenuItem>
<ContextMenuItem>Subscription</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>Komposition#
Verwende die folgende Komposition, um ein ContextMenu zu erstellen:
ContextMenu
├── ContextMenuTrigger
└── ContextMenuContent
├── ContextMenuGroup
│ ├── ContextMenuLabel
│ ├── ContextMenuItem
│ └── ContextMenuItem
├── ContextMenuSeparator
├── ContextMenuGroup
│ ├── ContextMenuLabel
│ ├── ContextMenuCheckboxItem
│ └── ContextMenuCheckboxItem
├── ContextMenuSeparator
├── ContextMenuGroup
│ ├── ContextMenuLabel
│ └── ContextMenuRadioGroup
│ ├── ContextMenuRadioItem
│ └── ContextMenuRadioItem
└── ContextMenuSub
├── ContextMenuSubTrigger
└── ContextMenuSubContent
└── ContextMenuGroup
├── ContextMenuItem
└── ContextMenuItemBeispiele#
Grundlegend#
Ein einfaches Kontextmenü mit einigen Aktionen.
import {
ContextMenu,
ContextMenuContent,Untermenü#
Verwende ContextMenuSub, um sekundäre Aktionen zu verschachteln.
import {
ContextMenu,
ContextMenuContent,Tastenkürzel#
Füge ContextMenuShortcut hinzu, um Tastaturhinweise anzuzeigen.
import {
ContextMenu,
ContextMenuContent,Gruppen#
Gruppiere verwandte Aktionen und trenne sie mit Trennern.
import {
ContextMenu,
ContextMenuContent,Icons#
Kombiniere Icons mit Beschriftungen für schnelles Erfassen.
import {
ClipboardPasteIcon,
CopyIcon,Checkboxen#
Verwende ContextMenuCheckboxItem für Umschalter.
import {
ContextMenu,
ContextMenuCheckboxItem,Radio#
Verwende ContextMenuRadioItem für exklusive Auswahlmöglichkeiten.
"use client"
import * as React from "react"Destruktiv#
Verwende variant="destructive", um das Menüelement als destruktiv zu gestalten.
import { ArchiveIcon, PencilIcon, ShareIcon, TrashIcon } from "lucide-react"
import {Seiten#
Steuere die Platzierung des Untermenüs mit den Props side und align.
import {
ContextMenu,
ContextMenuContent,RTL#
Um RTL-Unterstützung in Neobrutalism zu aktivieren, siehe die RTL-Konfigurationsanleitung.
"use client"
import * as React from "react"Verwende side="inline-end", um das Menü auf der logisch rechten Seite des Triggers zu platzieren.
<ContextMenu>
<ContextMenuTrigger>Right click here</ContextMenuTrigger>
<ContextMenuContent side="inline-end">
<ContextMenuItem>Profile</ContextMenuItem>
<ContextMenuItem>Billing</ContextMenuItem>
<ContextMenuItem>Team</ContextMenuItem>
<ContextMenuItem>Subscription</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>Barrierefreiheit#
Das Kontextmenü folgt dem WAI-ARIA-Menu-Pattern: Das Popup rendert role="menu" mit menuitem-, menuitemcheckbox- und menuitemradio-Kindern; der Fokus wandert beim Öffnen ins Menü und kehrt beim Schließen zum Triggerbereich zurück. Es öffnet über das contextmenu-Event des Browsers, daher funktioniert Shift + F10 (oder die dedizierte Menütaste) immer dann, wenn der Triggerbereich fokussierbar ist – ergänze tabIndex={0} am Kind des Triggers, falls es das nicht von Haus aus ist.
Tastaturinteraktionen:
| Taste | Aktion |
|---|---|
Shift + F10 / Menütaste | Öffnet das Menü, wenn der Triggerbereich den Fokus hat |
ArrowDown / ArrowUp | Bewegt den Fokus zum nächsten / vorherigen Eintrag |
ArrowRight / ArrowLeft | Öffnet / schließt ein Untermenü (in RTL gespiegelt) |
Enter / Space | Aktiviert den fokussierten Eintrag |
Esc | Schließt das Menü |
| Zeichen tippen | Bewegt den Fokus zum nächsten passenden Eintrag (Typeahead) |
API-Referenz#
Weitere Informationen findest du in der Base UI-Dokumentation.