Components
Context Menu
A right-click context menu, wrapping Morphos's ContextMenu primitive.
Installation
npx kosmesis add context-menupnpm dlx kosmesis add context-menuyarn dlx kosmesis add context-menubunx kosmesis add context-menuInstall the following dependencies:
npm install @morphos/overlayspnpm add @morphos/overlaysyarn add @morphos/overlaysbun add @morphos/overlaysCopy and paste the following code into your project.
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/csspnpm add @morphos/overlays @praxisjs/cssyarn add @morphos/overlays @praxisjs/cssbun add @morphos/overlays @praxisjs/cssCopy and paste the following code into your project.
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>
</>
);
}
}