Context Menu

Displays a menu located at the pointer, triggered by a right-click or a long-press.

Right click 👇

tailus-logo.png

import { Archive, ChevronRight, Trash } from "lucide-react";
import ContextMenu from "@tailus-ui/ContextMenu";
import { Caption } from "@tailus-ui/typography";

export const Overview = () => (
    <ContextMenu.Root>
      <div>
        <Caption align="center">Right click 👇</Caption>
        <ContextMenu.Trigger className="mt-4 mx-auto w-fit p-[--card-padding] rounded-[--card-radius] border border-dashed">
            <Caption size="xs" neutral>tailus-logo.png</Caption>
        </ContextMenu.Trigger>
      </div>

      <ContextMenu.Portal>
        <ContextMenu.Content mixed data-shade="800" variant="solid" intent="primary" className="min-w-56">
          <ContextMenu.Item>
            Scale <ContextMenu.Command>⌘S</ContextMenu.Command>
          </ContextMenu.Item>
          <ContextMenu.Item>
            Set to Primary <ContextMenu.Command>⌘P</ContextMenu.Command>
          </ContextMenu.Item>
          <ContextMenu.Separator/>
          <ContextMenu.Item>Copy </ContextMenu.Item>
          <ContextMenu.Item>Share... </ContextMenu.Item>
          <ContextMenu.Separator/>
          <ContextMenu.Sub>
            <ContextMenu.SubTrigger>
                Download
                <ChevronRight className="size-4 ml-auto"/>
            </ContextMenu.SubTrigger>
            <ContextMenu.Portal>
              <ContextMenu.SubContent mixed data-shade="800" variant="solid" intent="primary" className="min-w-fit" sideOffset={2} alignOffset={-5}>
                <ContextMenu.Item>Logomark </ContextMenu.Item>
                <ContextMenu.Item>Logotype </ContextMenu.Item>
                <ContextMenu.Separator/>
                <ContextMenu.Item>All </ContextMenu.Item>
              </ContextMenu.SubContent>
            </ContextMenu.Portal>
          </ContextMenu.Sub>
          <ContextMenu.Separator/>
          <ContextMenu.Item disabled intent="warning">
                <ContextMenu.Icon>
                    <Archive className="size-4"/>
                </ContextMenu.Icon>
                Archive
                <ContextMenu.Command>⌘A</ContextMenu.Command>
            </ContextMenu.Item>
            <ContextMenu.Item intent="danger">
                <Trash className="size-4"/>
                Delete
                <ContextMenu.Command>⌘D</ContextMenu.Command>
            </ContextMenu.Item>
        </ContextMenu.Content>
        </ContextMenu.Portal>
    </ContextMenu.Root>
)

Installation

Install the primitive and Copy-Paste the component code in a .tsx file.

npm install @radix-ui/react-context-menu
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import React from "react";
import {
  menu,
  menuSeparator as separator,
  defaultMenuProps,
  type MenuProps,
  type MenuSeparatorProps as SeparatorProps,
} from "@tailus/themer"

const MenuContext = React.createContext(defaultMenuProps);

const ContextMenuRoot = ContextMenuPrimitive.Root;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuLabel = ContextMenuPrimitive.Label;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuCheckboxItem = ContextMenuPrimitive.CheckboxItem;
const ContextMenuItemIndicator = ContextMenuPrimitive.ItemIndicator;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuRadioItem = ContextMenuPrimitive.RadioItem;
const ContextMenuSub = ContextMenuPrimitive.Sub;

export interface ContextMenuContentProps extends MenuProps { }

const ContextMenuTrigger = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Trigger>
  >((props, forwardedRef) => {
    const {trigger} = menu.solid({intent:"primary"});
  return (
    <ContextMenuPrimitive.Trigger
      {...props}
      ref={forwardedRef}
      className={trigger({className: props.className})}
    />
  )
});

const ContextMenuContent = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> & ContextMenuContentProps
  >(({ className, variant, intent, mixed, fancy, ...props }, forwardedRef) => {
    
  const {
    variant: contextVariant,
    intent: contextIntent,
    mixed: contextMixed,
    fancy: contextFancy,
  } = React.useContext(MenuContext);

  variant = variant || contextVariant || "soft";
  intent = intent || contextIntent;
  fancy = fancy || contextFancy;
  mixed = mixed || contextMixed;
    
  if (fancy && mixed) throw new Error('The fancy and mixed props cannot be used together.');

  const contextValues = { variant, intent, fancy, mixed };
  const { content } = menu[variant]();
    
  return (
    <MenuContext.Provider value={contextValues}>
      <ContextMenuPrimitive.Content
        {...props}
        ref={forwardedRef}
        className={content({fancy, mixed, className})}
      />
    </MenuContext.Provider>
  );
});

const ContextMenuItem = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & MenuProps
>((
  {
    className,
    intent,
    variant,
    ...props
  }, forwardedRef
) => {
  const {
    variant: contextVariant,
    intent: contextIntent
  } = React.useContext(MenuContext);

  variant = variant || contextVariant || "soft";
  intent = intent || contextIntent;

  const { item } = menu[variant]({intent});

  return (
    <ContextMenuPrimitive.Item
      {...props}
      ref={forwardedRef}
      className={item({intent, className})}
    />
  );
});

interface ContextMenuSubTriggerProps extends MenuProps {
}

const ContextMenuSubTrigger = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & ContextMenuSubTriggerProps
>((
  {
    className,
    intent,
    variant,
    ...props
  }, forwardedRef
) => {
  const {
    variant: contextVariant,
    intent: contextIntent
  } = React.useContext(MenuContext);

  variant = variant || contextVariant || "soft";
  intent = intent || contextIntent;

  const { subTrigger } = menu[variant]({intent});
  return (
    <ContextMenuPrimitive.SubTrigger
      {...props}
      ref={forwardedRef}
      className={subTrigger({intent, className})}
    />
  );
});

interface ContextMenuSubContentProps extends ContextMenuContentProps {
}

const ContextMenuSubContent = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> & ContextMenuSubContentProps
>((
  {
    className,
    variant = "soft",
    intent = "gray",
    mixed,
    fancy,
    ...props
  }, forwardedRef
) => {
  const { variant: contextVariant, fancy: contextFancy, mixed: contextMixed } = React.useContext(MenuContext);
  
  variant = variant || contextVariant || "soft";
  fancy = fancy || contextFancy;
  mixed = mixed || contextMixed;
  const { content } = menu[variant]({ intent });
  
  if (fancy && mixed) throw new Error('The fancy and mixed props cannot be used together.');
  
  return (
    <ContextMenuPrimitive.SubContent
      {...props}
      ref={forwardedRef}
      className={content({mixed, fancy, intent, className})}
    />
  );
});

interface ContextMenuSeparatorProps extends SeparatorProps {}

const ContextMenuSeparator = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> & ContextMenuSeparatorProps
>(({className, fancy, dashed, ...props}, forwardedRef) => {
  const {fancy: contextVariant} = React.useContext(MenuContext);
  fancy = fancy || contextVariant;

  if (fancy && dashed) throw new Error('A separator cannot be both fancy and dashed.');

  return (
    <ContextMenuPrimitive.Separator
      {...props}
      ref={forwardedRef}
      className={separator({fancy, dashed, className})}
    />
  );
});

const ContextMenuCommand = React.forwardRef<
  React.ElementRef<"div">,
  React.ComponentPropsWithoutRef<"div"> & ContextMenuContentProps
>(({className, ...props}, forwardedRef) => {
  const { command } = menu.solid({});
  return (
    <div
      {...props}
      ref={forwardedRef}
      className={command({className})}
    />
  );
});

const ContextMenuIcon = React.forwardRef<
  React.ElementRef<"div">,
  React.ComponentPropsWithoutRef<"div"> & ContextMenuContentProps
>(({className, ...props}, forwardedRef) => {
  const { icon } = menu.solid({});
  return (
    <div
      {...props}
      ref={forwardedRef}
      className={icon({className})}
    />
  );
});

export default {
  Root: ContextMenuRoot,
  Trigger: ContextMenuTrigger,
  Portal: ContextMenuPortal,
  Content: ContextMenuContent,
  Label: ContextMenuLabel,
  Item: ContextMenuItem,
  Group: ContextMenuGroup,
  CheckboxItem: ContextMenuCheckboxItem,
  ItemIndicator: ContextMenuItemIndicator,
  RadioGroup: ContextMenuRadioGroup,
  RadioItem: ContextMenuRadioItem,
  Sub: ContextMenuSub,
  SubTrigger: ContextMenuSubTrigger,
  SubContent: ContextMenuSubContent,
  Separator: ContextMenuSeparator,
  Command: ContextMenuCommand,
  Icon: ContextMenuIcon,
}

export {
  ContextMenuRoot,
  ContextMenuTrigger,
  ContextMenuPortal,
  ContextMenuContent,
  ContextMenuLabel,
  ContextMenuItem,
  ContextMenuGroup,
  ContextMenuCheckboxItem,
  ContextMenuItemIndicator,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSub,
  ContextMenuSubTrigger,
  ContextMenuSubContent,
  ContextMenuSeparator,
  ContextMenuCommand,
  ContextMenuIcon,
}

Usage

Import the ContextMenu parts you need to use in your component and build your Context Menu.

import ContextMenu from "@tailus-ui/ContextMenu";
export default () => (
  <ContextMenu.Root>
    <ContextMenu.Trigger />

    <ContextMenu.Portal>
      <ContextMenu.Content>
        <ContextMenu.Label />
        <ContextMenu.Item />

        <ContextMenu.Group>
          <ContextMenu.Item>
            <ContextMenu.Icon />
            // ...
            <ContextMenu.Command />
          </ContextMenu.Item>
        </ContextMenu.Group>

        <ContextMenu.Sub>
          <ContextMenu.SubTrigger />
          <ContextMenu.Portal>
            <ContextMenu.SubContent />
          </ContextMenu.Portal>
        </ContextMenu.Sub>

        <ContextMenu.Separator />
      </ContextMenu.Content>
    </ContextMenu.Portal>
  </ContextMenu.Root>
);

Reference

Content

The content of the ContextMenu component

Prop
Type
Default
variant
enum
soft
intent
enum
soft
mixed
boolean
-
fancy
boolean
-

Item

The item of the ContextMenu component

Prop
Type
Default
variant
enum
soft
intent
enum
gray

Icon

The icon of the Item component

Command

The component that dispalys the command Keys of the Item component

Examples