Skip to content

Context Menu

A floating right-click style menu with items, keyboard shortcuts, dividers and a danger variant.

A floating right-click style menu with items, keyboard shortcuts, dividers and a danger variant. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

Root class newt-context-menu
import {
  ContextMenu,
  ContextMenuDivider,
  ContextMenuItem,
  ContextMenuShortcut,
} from "@/registry/bases/newt/ui/context-menu"

export default function ContextMenuDemo() {
  return (
    <ContextMenu>
      <ContextMenuItem>Action one</ContextMenuItem>
      <ContextMenuItem>
        Action two
        <ContextMenuShortcut>Ctrl+K</ContextMenuShortcut>
      </ContextMenuItem>
      <ContextMenuDivider />
      <ContextMenuItem variant="danger">Delete</ContextMenuItem>
    </ContextMenu>
  )
}
<script setup lang="ts">
import {
  ContextMenu,
  ContextMenuDivider,
  ContextMenuItem,
  ContextMenuShortcut,
} from "~~/registry/bases/newt/ui/context-menu"
</script>

<template>
  <ContextMenu>
    <ContextMenuItem>Action one</ContextMenuItem>
    <ContextMenuItem>
      Action two
      <ContextMenuShortcut>Ctrl+K</ContextMenuShortcut>
    </ContextMenuItem>
    <ContextMenuDivider />
    <ContextMenuItem variant="danger">Delete</ContextMenuItem>
  </ContextMenu>
</template>

Installation

pnpm dlx newtui@latest add context-menu
npx newtui@latest add context-menu
yarn dlx newtui@latest add context-menu
bunx newtui@latest add context-menu

Install the dependencies

pnpm dlx --yes class-variance-authority
npx --yes class-variance-authority
yarn dlx --yes class-variance-authority
bunx --yes class-variance-authority

Copy the source into your project

import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"

import { cn } from "@/lib/utils"

const ContextMenu = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    role="menu"
    className={cn(
      "w-[188px] rounded-md border border-newt-border bg-newt-bg-floating p-1.5 shadow-elevation-high",
      className
    )}
    {...props}
  />
))
ContextMenu.displayName = "ContextMenu"

const contextMenuItemVariants = cva(
  "group flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-[13px] transition-colors duration-fast ease-newt hover:text-white",
  {
    variants: {
      variant: {
        default: "text-newt-text-secondary hover:bg-newt-brand",
        danger: "text-newt-dnd hover:bg-newt-dnd",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export interface ContextMenuItemProps
  extends
    React.ComponentProps<"div">,
    VariantProps<typeof contextMenuItemVariants> {}

const ContextMenuItem = React.forwardRef<HTMLDivElement, ContextMenuItemProps>(
  ({ className, variant, ...props }, ref) => (
    <div
      ref={ref}
      role="menuitem"
      tabIndex={-1}
      className={cn(contextMenuItemVariants({ variant }), className)}
      {...props}
    />
  )
)
ContextMenuItem.displayName = "ContextMenuItem"

const ContextMenuShortcut = React.forwardRef<
  HTMLSpanElement,
  React.ComponentProps<"span">
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    className={cn(
      "text-[11px] text-newt-text-muted group-hover:text-white/70",
      className
    )}
    {...props}
  />
))
ContextMenuShortcut.displayName = "ContextMenuShortcut"

const ContextMenuDivider = React.forwardRef<
  HTMLDivElement,
  React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    role="separator"
    className={cn("mx-1.5 my-1 h-px bg-newt-border", className)}
    {...props}
  />
))
ContextMenuDivider.displayName = "ContextMenuDivider"

export {
  ContextMenu,
  ContextMenuItem,
  ContextMenuShortcut,
  ContextMenuDivider,
  contextMenuItemVariants,
}
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div
    role="menu"
    :class="
      cn(
        'w-[188px] rounded-md border border-newt-border bg-newt-bg-floating p-1.5 shadow-elevation-high',
        props.class
      )
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

import { contextMenuItemVariants, type ContextMenuItemVariants } from "."

const props = defineProps<{
  class?: HTMLAttributes["class"]
  variant?: ContextMenuItemVariants["variant"]
}>()
</script>

<template>
  <div
    role="menuitem"
    tabindex="-1"
    :class="
      cn(contextMenuItemVariants({ variant: props.variant }), props.class)
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <span
    :class="
      cn(
        'text-[11px] text-newt-text-muted group-hover:text-white/70',
        props.class
      )
    "
  >
    <slot />
  </span>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div
    role="separator"
    :class="cn('mx-1.5 my-1 h-px bg-newt-border', props.class)"
  />
</template>
import { cva, type VariantProps } from "class-variance-authority"

export { default as ContextMenu } from "./ContextMenu.vue"
export { default as ContextMenuItem } from "./ContextMenuItem.vue"
export { default as ContextMenuShortcut } from "./ContextMenuShortcut.vue"
export { default as ContextMenuDivider } from "./ContextMenuDivider.vue"

export const contextMenuItemVariants = cva(
  "group flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-[13px] transition-colors duration-fast ease-newt hover:text-white",
  {
    variants: {
      variant: {
        default: "text-newt-text-secondary hover:bg-newt-brand",
        danger: "text-newt-dnd hover:bg-newt-dnd",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export type ContextMenuItemVariants = VariantProps<
  typeof contextMenuItemVariants
>

Update the import paths

The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.

Usage

import { ContextMenu } from "@/components/ui/context-menu"

export function Example() {
  return <ContextMenu />
}
<script setup lang="ts">
import { ContextMenu, ContextMenuItem, ContextMenuShortcut, ContextMenuDivider } from "@/components/ui/context-menu"
</script>

<template>
  <ContextMenu />
</template>

API reference

ContextMenu

PropType
children / default slot?React.ReactNode / Vue slot

Content rendered inside the component.

TypeReact.ReactNode / Vue slot
className / class?string

Additional classes merged with the component defaults.

Typestring

The registry exports ContextMenu, ContextMenuItem, ContextMenuShortcut, ContextMenuDivider. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Open the menu from both pointer and keyboard input, move focus into it, support arrow-key navigation, and restore focus to the trigger when it closes.

Tokens

Context Menu reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-context-menu.