Pathfinderby Commenda
Components

Dropdown

Recursive action menus with consistent keyboard behavior.

Usage

Example.tsx
import { Button, Dropdown, type DropdownMenu } from '@commenda/pathfinder';

const menu: DropdownMenu = {
  groups: [
    {
      id: 'actions',
      items: [
        { id: 'edit', type: 'action', label: 'Edit details', onClick: onEdit },
        {
          id: 'export',
          type: 'submenu',
          label: 'Export',
          menu: {
            groups: [
              {
                id: 'formats',
                items: [
                  {
                    id: 'csv',
                    type: 'action',
                    label: 'Export CSV',
                    onClick: onExportCsv,
                  },
                ],
              },
            ],
          },
        },
      ],
    },
  ],
};

<Dropdown
  trigger={<Button variant="secondary">Entity actions</Button>}
  menu={menu}
/>;

Installation

SH
pnpm --filter your-app add "@commenda/pathfinder@workspace:*"

The package currently lives in the Commenda workspace. External installation requires a published version or packed tarball. Import the precompiled stylesheet once; no application Tailwind setup is required.

API reference

PropTypeBehavior
triggerRequiredReactElement<any, string | React.JSXElementConstructor<any>>Required element forwarding refs and events; use Button or IconButton.
menuRequiredDropdownMenuView fieldsRequired menu definition. See the object fields and menu structure below.
openConditionalbooleanControlled visibility. Provide onOpenChange when using this prop.
defaultOpenbooleanInitial uncontrolled visibility; defaults to false.
onOpenChangeConditional(open: boolean) => voidCalled when visibility changes. Required in controlled mode.
sideSidePreferred side; defaults to bottom. Adjusts for viewport collisions.
alignAlignPreferred alignment; defaults to start.
sideOffsetnumberGap from the trigger in pixels; defaults to 4.
portalContainerHTMLElement|ShadowRoot|React.RefObject<HTMLElement | ShadowRoot | null>|nullOptional destination for the menu portal.
disabledbooleanDisables the trigger.
disabledReasonstringExplanation for an unavailable trigger.

menu.groups is an array of DropdownGroup objects:

FieldTypeBehavior
idstringRequired stable group identity.
labelstringOptional visible group heading.
itemsDropdownItem[]Required ordered menu items.

Every item has these shared fields:

FieldTypeBehavior
idstringRequired stable identity within its group.
labelstringRequired visible text and typeahead label.
leadingIconReactNodeOptional decorative icon.
descriptionstringOptional supporting text.
trailingTextstringOptional display hint; does not register a shortcut.
disabledbooleanPrevents activation.
disabledReasonstringExplanation for an unavailable item.

Choose exactly one item kind:

KindRequired fieldBehavior
actiononClick: () => voidRuns an action. Supports optional destructive.
linklinkConfig: NavigationLinkConfigNavigates to a URL.
submenumenu: DropdownMenuOpens another menu using the same recursive structure.
  • type: 'action' requires onClick, permits destructive, and cannot have menu.
  • type: 'link' requires linkConfig: { href; target?; rel?; render? } and renders a native anchor. Its optional render adapter forwards anchor props and ref for Next Link. It cannot have onClick or a submenu.
  • type: 'submenu' requires another DropdownMenu and cannot have onClick or destructive.

DROPDOWN-RECURSION-01: Supply an acyclic tree with stable sibling IDs. Submenus recurse with the same rules at every level; there is no fixed depth limit. Prefer shallow menus for usability. Shortcuts in trailing text are display hints, not registered key bindings. Actions close the menu by default; callbacks work from both pointer and keyboard.

Keyboard and accessibility

Enter/Space/ArrowDown opens. Arrows navigate items; inline-end opens a submenu and inline-start returns (directions reverse in RTL). Escape closes the current submenu before its parent. Focus returns to the invoking trigger. Disabled branches never open or execute. Disabled reasons are visible/described on rows. Labels support typeahead. Portal positioning constrains menus to available viewport space.

DROPDOWN-SEMANTICS-01: This component invokes actions. Select and MultiSelect are separate field components using selection primitives. Their popup/row appearance can share private presentation; roles, keyboard behavior and value state cannot be copied from Menu. A checkmark alone does not define a Select. Dropdown itself does not expose search, selection values, checkbox rows, or remote loading props.

DROPDOWN-ACTIONS-01: Group related actions with dividers. Put destructive actions last, never first or alone, and confirm consequential destruction in the application. Use icons for actions; trailing text holds shortcuts, counts, formats or ages. No actions shows an empty message.

Import @commenda/pathfinder/styles.css once at the app root. Event handlers require a client component in Next.js App Router.

Design rules

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

  • DROPDOWN-SEMANTICS-01 — Pathfinder: Dropdown invokes actions. Select and MultiSelect have their own value, search and selection contracts and appropriate semantics. A checkmark does not determine the component role. Use Dropdown for commands, and Select or MultiSelect for field values.

  • DROPDOWN-RECURSION-01 — Pathfinder: acyclic menu data, stable sibling IDs, mutually exclusive action/link/submenu types, identical behavior at every recursive depth. Based on Kakashi's data approach, rebuilt over Base UI without private workspace runtime dependencies. Prefer shallow navigation even though the renderer has no fixed depth limit.

  • DROPDOWN-ACTIONS-01 — Figma: group with dividers; destructive action last, never first or alone, usually followed by application confirmation. Icons identify actions; flags identify entities/jurisdictions. Trailing text is right-aligned shortcut/count/format/age metadata. Only one row is highlighted. Open below the trigger with collision handling. Pointer opening may animate from the trigger; keyboard opening is immediate.

On this page