FewTone UI
ComponentsDocs
  • Home

Getting started

  • Introduction
  • Installation
  • MCP Server
  • Smooth UI Skill
  • Roadmap

Components46

  • Accordion
  • Alert Dialog
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Calendar
  • Card
  • Checkbox
  • Combobox
  • Command
  • Context Menu
  • Copy Button
  • Date Picker
  • Dialog
  • Dropdown Menu
  • Hover Card
  • Input
  • Input OTP
  • Loading Button
  • Marquee
  • Navigation Menu
  • Number Input
  • Number Ticker
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Scroll Area
  • Select
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Spinner
  • Stepper
  • Switch
  • Switcher
  • Table
  • Tabs
  • Text Effects
  • Textarea
  • Theme Toggle
  • Toast
  • Toggle Group
  • Tooltip
© 2026 FewTone · MIT licenceGitHub
  1. Components
  2. Context Menu

Context Menu

pnpm dlx shadcn@latest add …/r/context-menu.json

An accessible React context menu for Next.js: a right-click menu with a gliding highlight, shortcuts, checkbox and radio items and sub-menus, built on Radix.

Right-click here
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"

export function FileActions() {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="rounded-lg border p-6" tabIndex={0}>
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent className="w-48">
        <ContextMenuItem>
          Rename
          <ContextMenuShortcut>F2</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem>Duplicate</ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem variant="destructive">Delete</ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
default.tsx
"use client";

import {
  ContextMenu,
  ContextMenuCheckboxItem,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { useState } from "react";

const sizes = [
  { value: "small", label: "Small" },
  { value: "default", label: "Default" },
  { value: "large", label: "Large" },
];

/** A page's right-click menu. Settings and choices keep it open, so you see them change. */
export default function ContextMenuDefault() {
  const [bookmarks, setBookmarks] = useState(true);
  const [fullUrls, setFullUrls] = useState(false);
  const [size, setSize] = useState("default");
  const keepOpen = (event: Event) => event.preventDefault();

  return (
    <ContextMenu>
      {/* Focusable, so the keyboard's context-menu key or Shift+F10 opens it too. The border firms up while it's open. */}
      <ContextMenuTrigger
        tabIndex={0}
        className="flex h-36 w-full max-w-72 items-center justify-center rounded-lg border border-dashed border-input text-sm text-muted-foreground transition-colors duration-(--duration-fast) ease-out select-none data-[state=open]:border-ring data-[state=open]:text-foreground"
      >
        Right-click here
      </ContextMenuTrigger>
      <ContextMenuContent className="w-56">
        <ContextMenuItem>
          Back
          <ContextMenuShortcut>⌘[</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem disabled>
          Forward
          <ContextMenuShortcut>⌘]</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuItem>
          Reload
          <ContextMenuShortcut>⌘R</ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuSub>
          <ContextMenuSubTrigger>More tools</ContextMenuSubTrigger>
          <ContextMenuSubContent className="w-48">
            <ContextMenuItem>Save page as…</ContextMenuItem>
            <ContextMenuItem>Create shortcut…</ContextMenuItem>
            <ContextMenuSeparator />
            <ContextMenuItem>Developer tools</ContextMenuItem>
          </ContextMenuSubContent>
        </ContextMenuSub>
        <ContextMenuSeparator />
        <ContextMenuCheckboxItem checked={bookmarks} onCheckedChange={setBookmarks} onSelect={keepOpen}>
          Show bookmarks bar
        </ContextMenuCheckboxItem>
        <ContextMenuCheckboxItem checked={fullUrls} onCheckedChange={setFullUrls} onSelect={keepOpen}>
          Show full URLs
        </ContextMenuCheckboxItem>
        <ContextMenuSeparator />
        <ContextMenuLabel>Text size</ContextMenuLabel>
        <ContextMenuRadioGroup value={size} onValueChange={setSize}>
          {sizes.map(({ value, label }) => (
            <ContextMenuRadioItem key={value} value={value} onSelect={keepOpen}>
              {label}
            </ContextMenuRadioItem>
          ))}
        </ContextMenuRadioGroup>
      </ContextMenuContent>
    </ContextMenu>
  );
}

Installation

  1. 1

    Install the dependencies.

    Package manager
    $ pnpm add lucide-react radix-ui
  2. 2

    Add the FewTone UI tokens to your global CSS file. Skip any variable your project already defines.

    app/globals.css
    :root {
      --size-control-sm: 2rem;
      --size-control-md: 2.25rem;
      --size-control-lg: 2.5rem;
      --size-icon-sm: 0.875rem;
      --size-icon-md: 1rem;
      --size-icon-lg: 1.25rem;
      --size-roll-fade: 0.25em;
      --size-roll-fade-x: 0.5em;
      --duration-instant: 80ms;
      --duration-fast: 150ms;
      --duration-base: 220ms;
      --duration-slow: 350ms;
      --duration-slower: 500ms;
      --z-overlay: 1200;
      --z-modal: 1300;
      --z-popover: 1400;
      --z-toast: 1500;
      --z-tooltip: 1600;
      --focus-ring-width: 2px;
      --focus-ring-offset: 2px;
      --opacity-disabled: 0.5;
      --ease-spring: linear(0, 0.157, 0.48, 0.77, 0.97, 1.07, 1.1, 1.09, 1.05, 1.02, 1, 0.995, 1);
      --destructive-foreground: oklch(1 0 0);
      --overlay: color-mix(in oklch, oklch(0 0 0) 40%, transparent);
    }
    
    .dark {
      --destructive-foreground: oklch(1 0 0);
      --overlay: color-mix(in oklch, oklch(0 0 0) 60%, transparent);
    }
    
    @theme inline {
      --animate-popover-in: popover-in var(--duration-fast) cubic-bezier(0.22, 1, 0.36, 1) both;
      --animate-popover-out: popover-out var(--duration-instant) cubic-bezier(0.55, 0, 1, 0.45) both;
      --animate-collapsible-open: collapsible-open var(--duration-slow) cubic-bezier(0.22, 1, 0.36, 1);
      --animate-collapsible-close: collapsible-close var(--duration-base) cubic-bezier(0.55, 0, 1, 0.45) both;
      --color-destructive-foreground: var(--destructive-foreground);
      --color-overlay: var(--overlay);
      @keyframes popover-in {
        from {
          opacity: 0;
          filter: blur(6px);
          transform: scale(0.96);
        }
        to {
          opacity: 1;
          filter: blur(0);
          transform: scale(1);
        }
      }
      @keyframes popover-out {
        from {
          opacity: 1;
          filter: blur(0);
          transform: scale(1);
        }
        to {
          opacity: 0;
          filter: blur(6px);
          transform: scale(0.96);
        }
      }
      @keyframes collapsible-open {
        from {
          height: 0;
          opacity: 0;
          filter: blur(6px);
        }
        to {
          height: var(--collapsible-height);
          opacity: 1;
          filter: blur(0);
        }
      }
      @keyframes collapsible-close {
        from {
          height: var(--collapsible-height);
          opacity: 1;
          filter: blur(0);
        }
        to {
          height: 0;
          opacity: 0;
          filter: blur(6px);
        }
      }
    }
    
    @media (prefers-reduced-motion: reduce) {
      :root {
        --duration-instant: 1ms;
        --duration-fast: 1ms;
        --duration-base: 1ms;
        --duration-slow: 1ms;
        --duration-slower: 1ms;
      }
    }
  3. 3

    Add Dropdown Menu first.

  4. 4

    Copy the source into your project.

    components/ui/context-menu.tsx
    "use client";
    
    import {
      MenuCheck,
      MenuSubContext,
      MenuSurface,
      menuClasses,
      menuItemVariants,
      useMenuRow,
      useMenuSub,
    } from "@/components/ui/dropdown-menu";
    import { cn } from "@/lib/utils";
    import { ChevronRightIcon } from "lucide-react";
    import { ContextMenu as ContextMenuPrimitive, Slot } from "radix-ui";
    import { type ComponentProps, useContext } from "react";
    
    /*
     * Context Menu, built on Radix Context Menu: a right-click (or a long-press on touch) opens it at the pointer.
     * On a focused trigger, the keyboard's context-menu key and Shift+F10 open it too. Inside, the keys are
     * Dropdown Menu's: arrow keys, Home/End, typeahead, sub-menus on the arrow keys, Escape.
     * - Dropdown Menu's twin: the surface, rows, gliding highlight and marks come from dropdown-menu.tsx, so the
     *   two menus never drift apart. Installing Context Menu brings Dropdown Menu with it.
     * - It grows from the pointer: Radix puts the transform origin at the corner by the pointer, and the popover
     *   keyframes scale from there through the blur.
     */
    
    function ContextMenu(props: ComponentProps<typeof ContextMenuPrimitive.Root>) {
      return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
    }
    
    /** The area that opens the menu. Make it focusable (tabIndex={0}) so keyboard users can open it too. */
    function ContextMenuTrigger({ className, ...props }: ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
      return (
        <ContextMenuPrimitive.Trigger
          data-slot="context-menu-trigger"
          className={cn(
            "outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)",
            className,
          )}
          {...props}
        />
      );
    }
    
    function ContextMenuPortal(props: ComponentProps<typeof ContextMenuPrimitive.Portal>) {
      return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />;
    }
    
    function ContextMenuContent({
      className,
      children,
      ...props
    }: ComponentProps<typeof ContextMenuPrimitive.Content>) {
      return (
        <ContextMenuPrimitive.Portal>
          <ContextMenuPrimitive.Content data-slot="context-menu-content" {...props} asChild>
            <MenuSurface
              className={cn(
                "max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin)",
                className,
              )}
            >
              {children}
            </MenuSurface>
          </ContextMenuPrimitive.Content>
        </ContextMenuPrimitive.Portal>
      );
    }
    
    function ContextMenuGroup(props: ComponentProps<typeof ContextMenuPrimitive.Group>) {
      return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />;
    }
    
    function ContextMenuLabel({
      className,
      inset,
      ...props
    }: ComponentProps<typeof ContextMenuPrimitive.Label> & { inset?: boolean }) {
      return (
        <ContextMenuPrimitive.Label
          data-slot="context-menu-label"
          data-inset={inset || undefined}
          className={cn(menuClasses.label, className)}
          {...props}
        />
      );
    }
    
    function ContextMenuItem({
      className,
      inset,
      variant = "default",
      children,
      onFocus,
      onBlur,
      ...props
    }: ComponentProps<typeof ContextMenuPrimitive.Item> & {
      /** Lines the label up with items that start with an icon */
      inset?: boolean;
      /** Destructive is red, and tints the highlight red as it lands */
      variant?: "default" | "destructive";
    }) {
      const row = useMenuRow({ tone: variant, onFocus, onBlur });
      return (
        <ContextMenuPrimitive.Item
          data-slot="context-menu-item"
          data-inset={inset || undefined}
          data-variant={variant}
          className={cn(menuItemVariants({ variant }), className)}
          onFocus={row.onFocus}
          onBlur={row.onBlur}
          {...props}
        >
          {row.highlight}
          {/* With asChild the child (a link) becomes the item and the highlight goes inside it */}
          <Slot.Slottable>{children}</Slot.Slottable>
        </ContextMenuPrimitive.Item>
      );
    }
    
    function ContextMenuCheckboxItem({
      className,
      children,
      checked,
      onFocus,
      onBlur,
      ...props
    }: ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
      const row = useMenuRow({ onFocus, onBlur });
      return (
        <ContextMenuPrimitive.CheckboxItem
          data-slot="context-menu-checkbox-item"
          checked={checked}
          className={cn(menuItemVariants(), menuClasses.choice, className)}
          onFocus={row.onFocus}
          onBlur={row.onBlur}
          {...props}
        >
          {row.highlight}
          <span className={menuClasses.indicator}>
            <MenuCheck checked={checked} />
          </span>
          {children}
        </ContextMenuPrimitive.CheckboxItem>
      );
    }
    
    function ContextMenuRadioGroup(props: ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
      return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />;
    }
    
    function ContextMenuRadioItem({
      className,
      children,
      onFocus,
      onBlur,
      ...props
    }: ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
      const row = useMenuRow({ onFocus, onBlur });
      return (
        <ContextMenuPrimitive.RadioItem
          data-slot="context-menu-radio-item"
          className={cn(menuItemVariants(), menuClasses.choice, className)}
          onFocus={row.onFocus}
          onBlur={row.onBlur}
          {...props}
        >
          {row.highlight}
          <span className={menuClasses.indicator}>
            {/* Always mounted, so the dot can grow in and shrink away rather than appear */}
            <ContextMenuPrimitive.ItemIndicator forceMount className={menuClasses.dot} />
          </span>
          {children}
        </ContextMenuPrimitive.RadioItem>
      );
    }
    
    function ContextMenuSeparator({
      className,
      ...props
    }: ComponentProps<typeof ContextMenuPrimitive.Separator>) {
      return (
        <ContextMenuPrimitive.Separator
          data-slot="context-menu-separator"
          className={cn(menuClasses.separator, className)}
          {...props}
        />
      );
    }
    
    /** A shortcut hint at the end of an item. It only shows the keys; binding them is up to you. */
    function ContextMenuShortcut({ className, ...props }: ComponentProps<"span">) {
      return (
        <span data-slot="context-menu-shortcut" className={cn(menuClasses.shortcut, className)} {...props} />
      );
    }
    
    function ContextMenuSub({
      open,
      defaultOpen,
      onOpenChange,
      ...props
    }: ComponentProps<typeof ContextMenuPrimitive.Sub>) {
      const sub = useMenuSub({ open, defaultOpen, onOpenChange });
      return (
        <MenuSubContext.Provider value={sub.open}>
          <ContextMenuPrimitive.Sub
            data-slot="context-menu-sub"
            open={sub.open}
            onOpenChange={sub.onOpenChange}
            {...props}
          />
        </MenuSubContext.Provider>
      );
    }
    
    function ContextMenuSubTrigger({
      className,
      inset,
      children,
      onFocus,
      onBlur,
      ...props
    }: ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { inset?: boolean }) {
      const open = useContext(MenuSubContext);
      const row = useMenuRow({ keep: open, onFocus, onBlur });
      return (
        <ContextMenuPrimitive.SubTrigger
          data-slot="context-menu-sub-trigger"
          data-inset={inset || undefined}
          className={cn(menuItemVariants(), menuClasses.subTrigger, className)}
          onFocus={row.onFocus}
          onBlur={row.onBlur}
          {...props}
        >
          {row.highlight}
          {children}
          <ChevronRightIcon aria-hidden className={menuClasses.chevron} />
        </ContextMenuPrimitive.SubTrigger>
      );
    }
    
    function ContextMenuSubContent({
      className,
      sideOffset = 4,
      // The surface's padding and border, so the sub-menu's first item lines up with its trigger
      alignOffset = -5,
      children,
      ...props
    }: ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
      return (
        // In a portal, so the menu's own scrolling and its keyframes' transform can't clip or carry it
        <ContextMenuPrimitive.Portal>
          <ContextMenuPrimitive.SubContent
            data-slot="context-menu-sub-content"
            sideOffset={sideOffset}
            alignOffset={alignOffset}
            {...props}
            asChild
          >
            <MenuSurface
              className={cn(
                "max-h-(--radix-context-menu-content-available-height) origin-(--radix-context-menu-content-transform-origin) shadow-lg",
                className,
              )}
            >
              {children}
            </MenuSurface>
          </ContextMenuPrimitive.SubContent>
        </ContextMenuPrimitive.Portal>
      );
    }
    
    export {
      ContextMenu,
      ContextMenuCheckboxItem,
      ContextMenuContent,
      ContextMenuGroup,
      ContextMenuItem,
      ContextMenuLabel,
      ContextMenuPortal,
      ContextMenuRadioGroup,
      ContextMenuRadioItem,
      ContextMenuSeparator,
      ContextMenuShortcut,
      ContextMenuSub,
      ContextMenuSubContent,
      ContextMenuSubTrigger,
      ContextMenuTrigger,
    };
  5. 5

    Update the import paths to match your project.

API reference

ContextMenu

The root. It opens where the pointer is, so there's no open prop. Also accepts every Radix Context Menu Root prop.

PropTypeDefaultDescription
onOpenChange?(open: boolean) => void–Called when the menu opens or closes.
modal?booleantrueWhile open, the rest of the page can't be scrolled or clicked, and screen readers only see the menu.
dir?"ltr" | "rtl"–Reading direction. In rtl, sub-menus open to the left.

ContextMenuTrigger

The area that opens the menu on right-click or long-press. Give it tabIndex={0} so keyboard users can open it with the context-menu key or Shift+F10.

PropTypeDefaultDescription
asChild?booleanfalseUse your own element, for example a card or a table row, instead of a span.
disabled?booleanfalseLeaves the browser's own menu.

ContextMenuContent

The menu. It grows from the pointer through the blur, and moves to stay on screen. It scrolls when it's taller than the space left. Renders in a portal.

PropTypeDefaultDescription
alignOffset?number0Shifts it from the pointer, in px.
loop?booleanfalseArrow keys wrap from the last item to the first, and back.
collisionPadding?number0Space to keep from the screen's edges, in px.

ContextMenuItem

An action. One highlight glides to the item under the pointer or the keyboard, and fades out when the pointer leaves the items.

PropTypeDefaultDescription
variant?"default" | "destructive""default"Destructive is red, and the highlight takes a red tint as it lands on it.
inset?booleanfalseIndents the label to line up with items that start with an icon.
disabled?booleanfalseSkips the item.
onSelect?(event: Event) => void–Called when the item is picked with a click, Enter or Space. The menu then closes; call event.preventDefault() to keep it open.
textValue?string–The text typeahead matches, when the item's content isn't plain text.

ContextMenuCheckboxItem

An item that switches a setting on and off. Its check draws itself in at the end of the row.

PropTypeDefaultDescription
checked?boolean | "indeterminate"falseWhether it's on. Indeterminate draws a dash.
onCheckedChange?(checked: boolean) => void–Called with the new state when it's picked.
disabled?booleanfalseSkips the item.
onSelect?(event: Event) => void–Call event.preventDefault() to keep the menu open while switching.

ContextMenuRadioGroup

Holds radio items, one of which is picked.

PropTypeDefaultDescription
value?string–The picked item's value.
onValueChange?(value: string) => void–Called with the new value when another item is picked.

ContextMenuRadioItem

One choice in a radio group. Its dot grows in at the end of the row.

PropTypeDefaultDescription
valuestring–The value this item picks.
disabled?booleanfalseSkips the item.
onSelect?(event: Event) => void–Call event.preventDefault() to keep the menu open while choosing.

ContextMenuLabel

A heading for the items below it. It isn't focusable.

PropTypeDefaultDescription
inset?booleanfalseIndents it to line up with items that start with an icon.

ContextMenuGroup

Groups related items (role="group").

PropTypeDefaultDescription
childrenReactNode–The items, often after a ContextMenuLabel.

ContextMenuSeparator

A line between groups.

PropTypeDefaultDescription
className?string–Extra classes, merged with cn.

ContextMenuShortcut

A shortcut hint at the end of an item.

PropTypeDefaultDescription
childrenReactNode–The keys, for example ⌘R. It only shows them: bind the keys yourself.

ContextMenuSub

A sub-menu: wraps its trigger and its content.

PropTypeDefaultDescription
open?boolean–Whether the sub-menu is open, when you control it.
defaultOpen?booleanfalseOpens it at first.
onOpenChange?(open: boolean) => void–Called when it opens or closes.

ContextMenuSubTrigger

The item that opens a sub-menu, on hover, a click, Enter, Space or ArrowRight. It keeps the highlight while its sub-menu is open, and its chevron nudges towards it.

PropTypeDefaultDescription
inset?booleanfalseIndents the label to line up with items that start with an icon.
disabled?booleanfalseSkips the item.

ContextMenuSubContent

The sub-menu. It opens beside its trigger with its first item level with it, and has its own highlight. ArrowLeft goes back to the trigger; Escape closes the whole menu. Renders in a portal.

PropTypeDefaultDescription
sideOffset?number4Gap between the menu and the sub-menu, in px.
alignOffset?number-5Shifts it along the trigger, in px. The default lines its first item up with the trigger.

ContextMenuPortal

Renders its children into document.body. ContextMenuContent and ContextMenuSubContent already use it.

PropTypeDefaultDescription
container?HTMLElement | nulldocument.bodyWhere to render instead.

Related components

  • Dropdown Menu

    An accessible React dropdown menu for Next.js with a highlight that glides between items, shortcuts, checkbox and radio items and sub-menus, built on Radix.

  • Select

    An accessible React select with a gliding highlight, groups, labels and a scrollable list, built on Radix Select.

  • Alert Dialog

    An accessible React alert dialog for Next.js that asks people to confirm destructive actions, with focus starting on Cancel, built on Radix Alert Dialog.