Kosmesis
Components

Dropdown Menu

A menu with full keyboard navigation, wrapping Morphos's Dropdown primitive.

Installation

npx kosmesis add dropdown-menu
pnpm dlx kosmesis add dropdown-menu
yarn dlx kosmesis add dropdown-menu
bunx kosmesis add dropdown-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.

dropdown-menu.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Dropdown, DropdownItem as MorphosDropdownItem, DropdownMenu as MorphosDropdownMenu, DropdownTrigger,  type DropdownItemProps as MorphosDropdownItemProps,  type DropdownMenuProps as MorphosDropdownMenuProps,  type DropdownProps,  type DropdownTriggerProps } from "@morphos/overlays";import { cn } from "@/lib/utils";/** * shadcn/ui's `DropdownMenu`/`DropdownMenuTrigger` map to Morphos's `Dropdown`/`DropdownTrigger` * (Morphos aliases the whole component `Menu` internally, but exports it as `Dropdown`). Both are * re-exported directly, renamed — the root is always instantiated directly * (`@State() dropdown = new DropdownMenu()`), never mounted via JSX, so wrapping it would break * `.isOpen`/`.toggle()`/`.closeDropdown()`. */export { Dropdown as DropdownMenu, DropdownTrigger as DropdownMenuTrigger };export type { DropdownProps as DropdownMenuProps, DropdownTriggerProps as DropdownMenuTriggerProps };export type DropdownMenuContentProps = MorphosDropdownMenuProps;@Component()export class DropdownMenuContent extends StatelessComponent<DropdownMenuContentProps> {  render() {    const { class: cls, ...rest } = this.props;    return (      <MorphosDropdownMenu        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 DropdownMenuItemProps extends MorphosDropdownItemProps {  variant?: "default" | "destructive";  inset?: boolean;}@Component()export class DropdownMenuItem extends StatelessComponent<DropdownMenuItemProps> {  render() {    const { class: cls, variant = "default", inset, ...rest } = this.props;    return (      <MorphosDropdownItem        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 [&_svg]:text-destructive",          "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",          cls,        )}        {...rest}      />    );  }}export interface DropdownMenuLabelProps {  class?: string;  inset?: boolean;  children?: Children;}@Component()export class DropdownMenuLabel extends StatelessComponent<DropdownMenuLabelProps> {  render() {    const { class: cls, inset, children } = this.props;    return (      <div class={cn("px-2 py-1.5 text-sm font-medium", inset && "pl-8", cls)}>{children}</div>    );  }}export interface DropdownMenuSeparatorProps {  class?: string;}@Component()export class DropdownMenuSeparator extends StatelessComponent<DropdownMenuSeparatorProps> {  render() {    const { class: cls } = this.props;    return <div role="separator" class={cn("-mx-1 my-1 h-px bg-border", cls)} />;  }}export interface DropdownMenuShortcutProps {  class?: string;  children?: Children;}@Component()export class DropdownMenuShortcut extends StatelessComponent<DropdownMenuShortcutProps> {  render() {    const { class: cls, children } = this.props;    return (      <span class={cn("ml-auto text-xs tracking-widest text-muted-foreground", cls)}>{children}</span>    );  }}

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.

dropdown-menu.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, keyframes, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { Dropdown, DropdownItem as MorphosDropdownItem, DropdownMenu as MorphosDropdownMenu, DropdownTrigger,  type DropdownItemProps as MorphosDropdownItemProps,  type DropdownMenuProps as MorphosDropdownMenuProps,  type DropdownProps,  type DropdownTriggerProps } 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 DropdownMenuStyles 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 })    .on("& svg", { pointerEvents: "none", flexShrink: 0, width: "1rem", height: "1rem" });  $itemInset = this.css({ paddingLeft: "2rem" });  $itemDestructive = this.css({ color: t.destructive }).hover({ backgroundColor: `color-mix(in oklab, ${t.destructive} 10%, transparent)` });  $label = this.css({ padding: "0.375rem 0.5rem", fontSize: "0.875rem", fontWeight: 500 });  $separator = this.css({ margin: "0.25rem -0.25rem", height: "1px", backgroundColor: t.border });  $shortcut = this.css({ marginLeft: "auto", fontSize: "0.75rem", letterSpacing: "0.05em", color: t.mutedForeground });}/** * shadcn/ui's `DropdownMenu`/`DropdownMenuTrigger` map to Morphos's `Dropdown`/`DropdownTrigger` * (Morphos aliases the whole component `Menu` internally, but exports it as `Dropdown`). Both are * re-exported directly, renamed — the root is always instantiated directly * (`@State() dropdown = new DropdownMenu()`), never mounted via JSX, so wrapping it would break * `.isOpen`/`.toggle()`/`.closeDropdown()`. */export { Dropdown as DropdownMenu, DropdownTrigger as DropdownMenuTrigger };export type { DropdownProps as DropdownMenuProps, DropdownTriggerProps as DropdownMenuTriggerProps };export type DropdownMenuContentProps = MorphosDropdownMenuProps;@Component()export class DropdownMenuContent extends StatelessComponent<DropdownMenuContentProps> {  @Styled(DropdownMenuStyles) $s!: DropdownMenuStyles;  render() {    const { class: cls, ...rest } = this.props;    return <MorphosDropdownMenu class={cx(this.$s.$content, cls)} {...rest} />;  }}export interface DropdownMenuItemProps extends MorphosDropdownItemProps {  variant?: "default" | "destructive";  inset?: boolean;}@Component()export class DropdownMenuItem extends StatelessComponent<DropdownMenuItemProps> {  @Styled(DropdownMenuStyles) $s!: DropdownMenuStyles;  render() {    const { class: cls, variant = "default", inset, ...rest } = this.props;    return (      <MorphosDropdownItem        class={cx(this.$s.$item, inset && this.$s.$itemInset, variant === "destructive" && this.$s.$itemDestructive, cls)}        {...rest}      />    );  }}export interface DropdownMenuLabelProps {  class?: string;  inset?: boolean;  children?: Children;}@Component()export class DropdownMenuLabel extends StatelessComponent<DropdownMenuLabelProps> {  @Styled(DropdownMenuStyles) $s!: DropdownMenuStyles;  render() {    const { class: cls, inset, children } = this.props;    return <div class={cx(this.$s.$label, inset && this.$s.$itemInset, cls)}>{children}</div>;  }}export interface DropdownMenuSeparatorProps {  class?: string;}@Component()export class DropdownMenuSeparator extends StatelessComponent<DropdownMenuSeparatorProps> {  @Styled(DropdownMenuStyles) $s!: DropdownMenuStyles;  render() {    const { class: cls } = this.props;    return <div role="separator" class={cx(this.$s.$separator, cls)} />;  }}export interface DropdownMenuShortcutProps {  class?: string;  children?: Children;}@Component()export class DropdownMenuShortcut extends StatelessComponent<DropdownMenuShortcutProps> {  @Styled(DropdownMenuStyles) $s!: DropdownMenuStyles;  render() {    const { class: cls, children } = this.props;    return <span class={cx(this.$s.$shortcut, cls)}>{children}</span>;  }}

Examples

Usage

import { buttonVariants } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

@Component()
class AccountMenu extends StatefulComponent {
  @State() dropdown = new DropdownMenu();

  render() {
    return (
      <>
        <DropdownMenuTrigger dropdown={this.dropdown} class={buttonVariants({ variant: "outline" })}>
          Open menu
        </DropdownMenuTrigger>
        <DropdownMenuContent dropdown={this.dropdown} aria-label="Account menu">
          <DropdownMenuLabel>My Account</DropdownMenuLabel>
          <DropdownMenuSeparator />
          <DropdownMenuItem dropdown={this.dropdown} value="profile" onSelect={() => {}}>
            Profile
          </DropdownMenuItem>
          <DropdownMenuItem dropdown={this.dropdown} value="logout" variant="destructive" onSelect={() => {}}>
            Log out
          </DropdownMenuItem>
        </DropdownMenuContent>
      </>
    );
  }
}

DropdownMenu/DropdownMenuTrigger are Morphos's Dropdown/DropdownTrigger, re-exported and renamed to match shadcn/ui's naming.

On this page