Kosmesis
Components

Context Menu

A right-click context menu, wrapping Morphos's ContextMenu primitive.

Installation

npx kosmesis add context-menu
pnpm dlx kosmesis add context-menu
yarn dlx kosmesis add context-menu
bunx kosmesis add context-menu

Install the following dependencies:

npm install @morphos/overlays
pnpm add @morphos/overlays
yarn add @morphos/overlays
bun add @morphos/overlays

Copy and paste the following code into your project.

context-menu.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { ContextMenuContent as MorphosContextMenuContent, ContextMenuItem as MorphosContextMenuItem,  type ContextMenuContentProps as MorphosContextMenuContentProps,  type ContextMenuItemProps as MorphosContextMenuItemProps } from "@morphos/overlays";import { cn } from "@/lib/utils";/** * `ContextMenu` and `ContextMenuTrigger` are re-exported directly — the root is always * instantiated directly (`@State() contextMenu = new ContextMenu()`), never mounted via JSX, so * wrapping it would break `.isOpen`/`.open()`/`.close()`. `ContextMenuTrigger` adds no default * styling of its own (it's just the right-click target), so it's re-exported as-is too. */export {  ContextMenu,  ContextMenuTrigger,  type ContextMenuProps,  type ContextMenuTriggerProps,} from "@morphos/overlays";export type ContextMenuContentProps = MorphosContextMenuContentProps;@Component()export class ContextMenuContent extends StatelessComponent<ContextMenuContentProps> {  render() {    const { class: cls, ...rest } = this.props;    return (      <MorphosContextMenuContent        class={cn(          "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",          "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",          cls,        )}        {...rest}      />    );  }}export interface ContextMenuItemProps extends MorphosContextMenuItemProps {  variant?: "default" | "destructive";  inset?: boolean;}@Component()export class ContextMenuItem extends StatelessComponent<ContextMenuItemProps> {  render() {    const { class: cls, variant = "default", inset, ...rest } = this.props;    return (      <MorphosContextMenuItem        class={cn(          "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none",          "hover:bg-accent hover:text-accent-foreground",          "data-disabled:pointer-events-none data-disabled:opacity-50",          inset && "pl-8",          variant === "destructive" && "text-destructive hover:bg-destructive/10",          cls,        )}        {...rest}      />    );  }}export interface ContextMenuSeparatorProps {  class?: string;}@Component()export class ContextMenuSeparator extends StatelessComponent<ContextMenuSeparatorProps> {  render() {    const { class: cls } = this.props;    return <div role="separator" class={cn("-mx-1 my-1 h-px bg-border", cls)} />;  }}

Install the following dependencies:

npm install @morphos/overlays @praxisjs/css
pnpm add @morphos/overlays @praxisjs/css
yarn add @morphos/overlays @praxisjs/css
bun add @morphos/overlays @praxisjs/css

Copy and paste the following code into your project.

context-menu.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, keyframes, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { ContextMenuContent as MorphosContextMenuContent, ContextMenuItem as MorphosContextMenuItem,  type ContextMenuContentProps as MorphosContextMenuContentProps,  type ContextMenuItemProps as MorphosContextMenuItemProps } from "@morphos/overlays";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);const popIn = keyframes("kosmesis-pop-in", {  from: { opacity: "0", transform: "scale(0.95)" },  to: { opacity: "1", transform: "scale(1)" },});class ContextMenuStyles extends Stylesheet {  $content = this.css({    zIndex: 50,    minWidth: "8rem",    overflow: "hidden",    borderRadius: `calc(${t.radius} - 2px)`,    border: `1px solid ${t.border}`,    backgroundColor: t.popover,    padding: "0.25rem",    color: t.popoverForeground,    boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",  }).on("&[data-open]", { animation: `${popIn} 100ms ease-out` });  $item = this.css({    position: "relative",    display: "flex",    cursor: "default",    alignItems: "center",    gap: "0.5rem",    borderRadius: "0.125rem",    padding: "0.375rem 0.5rem",    fontSize: "0.875rem",    outline: "none",    userSelect: "none",  })    .hover({ backgroundColor: t.accent, color: t.accentForeground })    .on("&[data-disabled]", { pointerEvents: "none", opacity: 0.5 });  $itemInset = this.css({ paddingLeft: "2rem" });  $itemDestructive = this.css({ color: t.destructive }).hover({ backgroundColor: `color-mix(in oklab, ${t.destructive} 10%, transparent)` });  $separator = this.css({ margin: "0.25rem -0.25rem", height: "1px", backgroundColor: t.border });}/** * `ContextMenu` and `ContextMenuTrigger` are re-exported directly — the root is always * instantiated directly (`@State() contextMenu = new ContextMenu()`), never mounted via JSX, so * wrapping it would break `.isOpen`/`.open()`/`.close()`. `ContextMenuTrigger` adds no default * styling of its own (it's just the right-click target), so it's re-exported as-is too. */export { ContextMenu, ContextMenuTrigger, type ContextMenuProps, type ContextMenuTriggerProps } from "@morphos/overlays";export type ContextMenuContentProps = MorphosContextMenuContentProps;@Component()export class ContextMenuContent extends StatelessComponent<ContextMenuContentProps> {  @Styled(ContextMenuStyles) $s!: ContextMenuStyles;  render() {    const { class: cls, ...rest } = this.props;    return <MorphosContextMenuContent class={cx(this.$s.$content, cls)} {...rest} />;  }}export interface ContextMenuItemProps extends MorphosContextMenuItemProps {  variant?: "default" | "destructive";  inset?: boolean;}@Component()export class ContextMenuItem extends StatelessComponent<ContextMenuItemProps> {  @Styled(ContextMenuStyles) $s!: ContextMenuStyles;  render() {    const { class: cls, variant = "default", inset, ...rest } = this.props;    return (      <MorphosContextMenuItem        class={cx(this.$s.$item, inset && this.$s.$itemInset, variant === "destructive" && this.$s.$itemDestructive, cls)}        {...rest}      />    );  }}export interface ContextMenuSeparatorProps {  class?: string;}@Component()export class ContextMenuSeparator extends StatelessComponent<ContextMenuSeparatorProps> {  @Styled(ContextMenuStyles) $s!: ContextMenuStyles;  render() {    const { class: cls } = this.props;    return <div role="separator" class={cx(this.$s.$separator, cls)} />;  }}

Examples

Usage

import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from "@/components/ui/context-menu";

@Component()
class FileRow extends StatefulComponent {
  @State() menu = new ContextMenu();

  render() {
    return (
      <>
        <ContextMenuTrigger contextMenu={this.menu} class="rounded-md border border-dashed p-8 text-center text-sm">
          Right-click here
        </ContextMenuTrigger>
        <ContextMenuContent contextMenu={this.menu} aria-label="File actions">
          <ContextMenuItem contextMenu={this.menu} value="rename" onSelect={() => {}}>Rename</ContextMenuItem>
          <ContextMenuItem contextMenu={this.menu} value="delete" variant="destructive" onSelect={() => {}}>
            Delete
          </ContextMenuItem>
        </ContextMenuContent>
      </>
    );
  }
}

On this page