AZMARA PLATFORM
Components

Menu

DropdownMenu and ContextMenu. Menus of actions with items, checkbox and radio items, groups and submenus. Built on Base UI.

Usage

import {
  DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLinkItem,
  DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem,
  DropdownMenuGroup, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut,
  DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent,
  ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, // …and the same parts as above
} from "@azmr/ui/menu";

These are interactive, so they are client components. A menu renders in a portal on the --z-popover layer, so one opened from inside a dialog appears above it. It flips when there is no room and scrolls inside the space available.

Use a menu for actions (things that happen). For choosing a value in a form, use a select. For showing information, use a popover.

Preview theme
Preview density

Nothing chosen yet · grid on · comfortable

"use client";

import { Button } from "@azmr/ui/button";
import { Icon } from "@azmr/ui/icons";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@azmr/ui/menu";
import { ChevronDown } from "lucide-react";
import { useState } from "react";

export default function DropdownBasic() {
  const [showGrid, setShowGrid] = useState(true);
  const [density, setDensity] = useState("comfortable");
  const [last, setLast] = useState("Nothing chosen yet");

  return (
    <div className="flex items-center gap-4">
      <DropdownMenu>
        <DropdownMenuTrigger
          render={
            <Button variant="outline" endIcon={<Icon icon={ChevronDown} />}>
              Project
            </Button>
          }
        />
        <DropdownMenuContent>
          <DropdownMenuGroup>
            <DropdownMenuLabel>Project</DropdownMenuLabel>
            <DropdownMenuItem onClick={() => setLast("Saved")}>
              Save
              <DropdownMenuShortcut>Ctrl+S</DropdownMenuShortcut>
            </DropdownMenuItem>
            <DropdownMenuItem onClick={() => setLast("Renamed")}>Rename</DropdownMenuItem>
            <DropdownMenuItem disabled>Archive (needs an owner)</DropdownMenuItem>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuCheckboxItem checked={showGrid} onCheckedChange={setShowGrid}>
            Show grid
          </DropdownMenuCheckboxItem>
          <DropdownMenuRadioGroup value={density} onValueChange={(v) => setDensity(v as string)}>
            <DropdownMenuRadioItem value="comfortable">Comfortable</DropdownMenuRadioItem>
            <DropdownMenuRadioItem value="compact">Compact</DropdownMenuRadioItem>
          </DropdownMenuRadioGroup>
          <DropdownMenuSeparator />
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>Share</DropdownMenuSubTrigger>
            <DropdownMenuSubContent>
              <DropdownMenuItem onClick={() => setLast("Shared by email")}>Email</DropdownMenuItem>
              <DropdownMenuItem onClick={() => setLast("Link copied")}>Copy link</DropdownMenuItem>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuSeparator />
          <DropdownMenuItem variant="destructive" onClick={() => setLast("Deleted")}>
            Delete
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <p className="text-sm text-muted-foreground" aria-live="polite">
        {last} · grid {showGrid ? "on" : "off"} · {density}
      </p>
    </div>
  );
}

Items

PartWhat it is
DropdownMenuItemAn action. variant="destructive" for one that deletes or discards. disabled greys it out.
DropdownMenuLinkItemA real <a>, so middle click and "open in new tab" work.
DropdownMenuCheckboxItemToggles on and off. It stays open when toggled; set closeOnClick to change that.
DropdownMenuRadioGroup and DropdownMenuRadioItemExactly one selected.
DropdownMenuGroup and DropdownMenuLabelA named group. The label is not an item, so arrow keys skip it.
DropdownMenuSeparatorA divider.
DropdownMenuSub, …SubTrigger, …SubContentA nested menu.
DropdownMenuShortcutA shortcut hint at the end of an item. It is text only: wire up the real shortcut yourself, and add aria-keyshortcuts to the item.

Items keep the menu open when closeOnClick={false}.

Disabled items stay focusable

Following the WAI-ARIA menu pattern, a disabled item is announced as disabled and cannot be run, but arrow keys still land on it. That is deliberate: it lets people find out an action exists, and why it is unavailable if you say so in the label.

ContextMenu

Preview theme
Preview density
Right-click here

Right-click the card, or use the button

"use client";

import { Button } from "@azmr/ui/button";
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@azmr/ui/menu";
import { useState } from "react";

export default function ContextBasic() {
  const [last, setLast] = useState("Right-click the card, or use the button");
  const choose = (what: string) => () => setLast(what);

  return (
    <div className="flex flex-col items-start gap-3">
      <ContextMenu>
        <ContextMenuTrigger className="flex h-28 w-72 items-center justify-center rounded-lg border border-dashed border-border text-sm text-muted-foreground">
          Right-click here
        </ContextMenuTrigger>
        <ContextMenuContent>
          <ContextMenuItem onClick={choose("Copied")}>Copy</ContextMenuItem>
          <ContextMenuItem onClick={choose("Pasted")}>Paste</ContextMenuItem>
          <ContextMenuSeparator />
          <ContextMenuItem variant="destructive" onClick={choose("Deleted")}>
            Delete
          </ContextMenuItem>
        </ContextMenuContent>
      </ContextMenu>

      {/* The same actions on a visible button: a context menu must never be the only way in. */}
      <DropdownMenu>
        <DropdownMenuTrigger
          render={
            <Button variant="outline" size="sm">
              Actions
            </Button>
          }
        />
        <DropdownMenuContent>
          <DropdownMenuItem onClick={choose("Copied")}>Copy</DropdownMenuItem>
          <DropdownMenuItem onClick={choose("Pasted")}>Paste</DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem variant="destructive" onClick={choose("Deleted")}>
            Delete
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <p className="text-sm text-muted-foreground" aria-live="polite">
        {last}
      </p>
    </div>
  );
}

A context menu opens at the pointer on right click (and, on touch, a long press: Base UI provides that and it is not tested here). Once open it behaves exactly like a dropdown, and it takes the same item parts.

Use it as an enhancement only. The trigger area is not focusable, the browser's keyboard route to a context menu needs a focused element, and a long press is easy to miss. So everything in it must be reachable another way, as in the demo, where the same actions are on a visible button.

Open a dialog from a menu

Preview theme
Preview density

Choose Delete… to confirm.

"use client";

import { Button } from "@azmr/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@azmr/ui/menu";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@azmr/ui/modal";
import { useState } from "react";

// Open a dialog from a menu item: control the dialog and open it from the item's onClick. The
// dialog sits next to the menu, not inside it, so it survives the menu closing.
export default function MenuOpensDialog() {
  const [confirming, setConfirming] = useState(false);
  const [deleted, setDeleted] = useState(false);

  return (
    <div className="flex items-center gap-4">
      <DropdownMenu>
        <DropdownMenuTrigger render={<Button variant="outline">More</Button>} />
        <DropdownMenuContent>
          <DropdownMenuItem>Duplicate</DropdownMenuItem>
          <DropdownMenuItem variant="destructive" onClick={() => setConfirming(true)}>
            Delete…
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>

      <AlertDialog open={confirming} onOpenChange={setConfirming}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Delete this item?</AlertDialogTitle>
            <AlertDialogDescription>This cannot be undone.</AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Keep it</AlertDialogCancel>
            <AlertDialogAction onClick={() => setDeleted(true)}>Delete</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      <p className="text-sm text-muted-foreground" aria-live="polite">
        {deleted ? "Deleted." : "Choose Delete… to confirm."}
      </p>
    </div>
  );
}

Control the dialog and open it from the item's onClick. Keep the dialog next to the menu rather than inside it: it has to outlive the menu closing. Use this for any action that needs confirmation.

Keyboard

Keyboard interactions
KeyAction
EnterorSpaceorArrowDownOn the trigger: opens the menu on the first item.
ArrowUpOn the trigger: opens the menu on the last item.
ArrowDownorArrowUpMoves to the next or previous item and wraps at the ends.
HomeorEndJumps to the first or last item.
A–ZTypeahead: jumps to the item that starts with the letters you type. Letters typed together are one search.
EnterorSpaceRuns the focused item and closes the menu. A checkbox or radio item toggles.
ArrowRightOn a submenu trigger: opens the submenu and focuses its first item.
ArrowLeftIn a submenu: closes it and returns to its trigger.
EscapeCloses the open submenu, or the menu. Focus returns to the trigger.
TabCloses the menu and moves on. Focus is never trapped.

Accessibility

  • The popup is a menu; items are menuitem, menuitemcheckbox (with aria-checked) or menuitemradio. The trigger has aria-haspopup="menu" and aria-expanded.
  • A submenu trigger announces aria-haspopup="menu" and aria-expanded. Its chevron is decorative.
  • Check and radio marks are decorative; the state is aria-checked.
  • Destructive items use the destructive colour as text on the popover surface, which meets the contrast the tokens guarantee.

Motion

Menus fade and scale from the trigger on --motion-duration-fast, and collapse to instant under prefers-reduced-motion.

Props

Prop

Type

Prop

Type

The rest are Base UI's props, passed through (open, onOpenChange, loopFocus, modal, and so on).

On this page