import { DropdownMenu, Button } from "@photon-ai/kumo-solid";
import { PlusIcon } from "~/components/icons";

export function DropdownBasicDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={(renderProps) => (
          <Button {...renderProps} icon={PlusIcon}>
            Add
          </Button>
        )}
      />
      <DropdownMenu.Content>
        <DropdownMenu.Item>Worker</DropdownMenu.Item>
        <DropdownMenu.Item>Pages</DropdownMenu.Item>
        <DropdownMenu.Item>KV Namespace</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

Installation

Barrel

import { DropdownMenu } from "@photon-ai/kumo-solid";

Granular

import { DropdownMenu } from "@photon-ai/kumo-solid/components/dropdown";

Usage

import { DropdownMenu, Button } from "@photon-ai/kumo-solid";

export default function Example() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={(triggerProps) => <Button {...triggerProps}>Menu</Button>}
      />
      <DropdownMenu.Content>
        <DropdownMenu.Item onClick={() => console.log("edit")}>
          Edit
        </DropdownMenu.Item>
        <DropdownMenu.Item onClick={() => console.log("duplicate")}>
          Duplicate
        </DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item
          variant="danger"
          onClick={() => console.log("delete")}
        >
          Delete
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

Examples

Basic Dropdown

import { DropdownMenu, Button } from "@photon-ai/kumo-solid";
import { PlusIcon } from "~/components/icons";

export function DropdownBasicDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={(renderProps) => (
          <Button {...renderProps} icon={PlusIcon}>
            Add
          </Button>
        )}
      />
      <DropdownMenu.Content>
        <DropdownMenu.Item>Worker</DropdownMenu.Item>
        <DropdownMenu.Item>Pages</DropdownMenu.Item>
        <DropdownMenu.Item>KV Namespace</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

Inset Items

Use inset on items without an icon to align their text with items that have one.

import { DropdownMenu, Button } from "@photon-ai/kumo-solid";
import { CopyIcon, PencilSimpleIcon, TrashIcon } from "~/components/icons";

/**
 * Use `inset` on items without an icon to align their text with items that have one.
 */
export function DropdownInsetDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={(renderProps) => <Button {...renderProps}>Edit</Button>}
      />
      <DropdownMenu.Content>
        <DropdownMenu.Item icon={PencilSimpleIcon}>Rename</DropdownMenu.Item>
        <DropdownMenu.Item icon={CopyIcon}>Duplicate</DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item inset>Move to folder</DropdownMenu.Item>
        <DropdownMenu.Item inset>Add to favorites</DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item icon={TrashIcon} variant="danger">
          Delete
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

Handling Item Clicks

Use onClick on DropdownMenu.Item to handle actions. Each item receives a native MouseEvent.

import { createSignal } from "solid-js";
import { DropdownMenu, Button } from "@photon-ai/kumo-solid";
import { CopyIcon, PencilSimpleIcon, TrashIcon } from "~/components/icons";

/**
 * Use `onClick` on `DropdownMenu.Item` to handle item actions.
 * Each item receives a native MouseEvent.
 */
export function DropdownOnClickDemo() {
  const [lastAction, setLastAction] = createSignal<string | null>(null);

  return (
    <div class="flex flex-col items-start gap-2">
      <DropdownMenu>
        <DropdownMenu.Trigger
          render={(renderProps) => <Button {...renderProps}>Actions</Button>}
        />
        <DropdownMenu.Content>
          <DropdownMenu.Item
            icon={CopyIcon}
            onClick={() => setLastAction("Duplicated")}
          >
            Duplicate
          </DropdownMenu.Item>
          <DropdownMenu.Item
            icon={PencilSimpleIcon}
            onClick={() => setLastAction("Renamed")}
          >
            Rename
          </DropdownMenu.Item>
          <DropdownMenu.Separator />
          <DropdownMenu.Item
            icon={TrashIcon}
            variant="danger"
            onClick={() => setLastAction("Deleted")}
          >
            Delete
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu>
      {lastAction() && (
        <p class="text-sm text-kumo-subtle">
          Last action: <span class="text-kumo-default">{lastAction()}</span>
        </p>
      )}
    </div>
  );
}

Checkbox Items

Use DropdownMenu.CheckboxItem for toggleable options that can be independently checked or unchecked.

import { createSignal } from "solid-js";
import { DropdownMenu, Button } from "@photon-ai/kumo-solid";

export function DropdownCheckboxDemo() {
  const [showSidebar, setShowSidebar] = createSignal(true);
  const [showLineNumbers, setShowLineNumbers] = createSignal(false);
  const [wordWrap, setWordWrap] = createSignal(true);

  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={(renderProps) => <Button {...renderProps}>View Options</Button>}
      />
      <DropdownMenu.Content>
        <DropdownMenu.Group>
          <DropdownMenu.Label>Display</DropdownMenu.Label>
          <DropdownMenu.CheckboxItem
            checked={showSidebar()}
            onCheckedChange={setShowSidebar}
          >
            Show sidebar
          </DropdownMenu.CheckboxItem>
          <DropdownMenu.CheckboxItem
            checked={showLineNumbers()}
            onCheckedChange={setShowLineNumbers}
          >
            Show line numbers
          </DropdownMenu.CheckboxItem>
          <DropdownMenu.CheckboxItem
            checked={wordWrap()}
            onCheckedChange={setWordWrap}
          >
            Word wrap
          </DropdownMenu.CheckboxItem>
        </DropdownMenu.Group>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

Nested Menu with Radio Selection

Use DropdownMenu.Sub, DropdownMenu.SubTrigger, and DropdownMenu.SubContent to create nested submenus. For single-selection lists like language or timezone, use DropdownMenu.RadioGroup and DropdownMenu.RadioItem.

import { createSignal } from "solid-js";
import { DropdownMenu, Button } from "@photon-ai/kumo-solid";
import { UserIcon, CreditCardIcon, MoonIcon, SignOutIcon } from "~/components/icons";

export function DropdownNestedDemo() {
  const [language, setLanguage] = createSignal("en");
  const [timezone, setTimezone] = createSignal("America/Los_Angeles");

  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={(renderProps) => (
          <Button {...renderProps} icon={UserIcon}>
            Account
          </Button>
        )}
      />
      <DropdownMenu.Content>
        <DropdownMenu.Item icon={UserIcon}>Profile</DropdownMenu.Item>
        <DropdownMenu.Item icon={CreditCardIcon}>Billing</DropdownMenu.Item>
        <DropdownMenu.Item icon={MoonIcon}>Dark mode</DropdownMenu.Item>

        {/* Language submenu with RadioGroup */}
        <DropdownMenu.Sub>
          <DropdownMenu.SubTrigger>Language</DropdownMenu.SubTrigger>
          <DropdownMenu.SubContent>
            <DropdownMenu.Group>
              <DropdownMenu.RadioGroup
                value={language()}
                onValueChange={setLanguage}
              >
                {languages.map((lang) => (
                  <DropdownMenu.RadioItem value={lang.code}>
                    {lang.label}
                    <DropdownMenu.RadioItemIndicator />
                  </DropdownMenu.RadioItem>
                ))}
              </DropdownMenu.RadioGroup>
            </DropdownMenu.Group>
          </DropdownMenu.SubContent>
        </DropdownMenu.Sub>

        {/* Timezone submenu with RadioGroup */}
        <DropdownMenu.Sub>
          <DropdownMenu.SubTrigger>Set Timezone</DropdownMenu.SubTrigger>
          <DropdownMenu.SubContent>
            <DropdownMenu.Group>
              <DropdownMenu.RadioGroup
                value={timezone()}
                onValueChange={setTimezone}
              >
                {timezones.map((tz) => (
                  <DropdownMenu.RadioItem value={tz.value}>
                    {tz.label}
                    <DropdownMenu.RadioItemIndicator />
                  </DropdownMenu.RadioItem>
                ))}
              </DropdownMenu.RadioGroup>
            </DropdownMenu.Group>
          </DropdownMenu.SubContent>
        </DropdownMenu.Sub>

        <DropdownMenu.Separator />
        <DropdownMenu.Item icon={SignOutIcon} variant="danger">
          Log out
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

Custom Trigger with Avatar

Use the render prop to customize the trigger element while passing children to render inside it. This is useful when you need a non-button trigger (like an avatar) wrapped in an accessible button element.

import { DropdownMenu } from "@photon-ai/kumo-solid";
import { UserIcon, SignOutIcon, GearIcon } from "~/components/icons";

/**
 * Demonstrates using the render prop with children to compose a custom trigger
 * that contains other elements. The render prop provides the trigger element,
 * while children are rendered inside it.
 */
export function DropdownAvatarTriggerDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={(renderProps) => (
          <button {...renderProps} type="button" class="rounded-full" />
        )}
      >
        <span class="flex h-8 w-8 items-center justify-center rounded-full bg-kumo-brand text-sm font-medium text-white">
          MR
        </span>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.Item icon={UserIcon}>Profile</DropdownMenu.Item>
        <DropdownMenu.Item icon={GearIcon}>Settings</DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item icon={SignOutIcon} variant="danger">
          Log out
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

Use DropdownMenu.LinkItem for menu items that navigate to a URL. This renders a semantic <a> element with full control over link attributes like target and rel.

import { DropdownMenu, Button } from "@photon-ai/kumo-solid";
import { GearIcon, BookOpenIcon, ArrowSquareOutIcon } from "~/components/icons";

/**
 * Demonstrates the new LinkItem component for navigation links.
 * Use LinkItem instead of Item with href for cleaner, more semantic links.
 */
export function DropdownLinkItemDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={(renderProps) => <Button {...renderProps}>Resources</Button>}
      />
      <DropdownMenu.Content>
        <DropdownMenu.LinkItem href="/settings" icon={GearIcon}>
          Settings
        </DropdownMenu.LinkItem>
        <DropdownMenu.LinkItem href="/docs" icon={BookOpenIcon}>
          Documentation
        </DropdownMenu.LinkItem>
        <DropdownMenu.Separator />
        <DropdownMenu.LinkItem
          href="https://developers.cloudflare.com"
          target="_blank"
          icon={ArrowSquareOutIcon}
        >
          Developer Docs
        </DropdownMenu.LinkItem>
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

Long List

When a dropdown contains many items, the content automatically limits its height to the available viewport space and becomes scrollable.

import { DropdownMenu, Button } from "@photon-ai/kumo-solid";

/**
 * A dropdown with a very long list of items to demonstrate the max-height
 * behavior — the content scrolls when it exceeds available viewport space.
 */
export function DropdownLongListDemo() {
  const items = Array.from({ length: 30 }, (_, i) => `Option ${i + 1}`);

  return (
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={(renderProps) => (
          <Button {...renderProps}>Open long list</Button>
        )}
      />
      <DropdownMenu.Content>
        {items.map((item) => (
          <DropdownMenu.Item>{item}</DropdownMenu.Item>
        ))}
      </DropdownMenu.Content>
    </DropdownMenu>
  );
}

API Reference

Root component that manages the dropdown state.

PropTypeDefaultDescription
variant"default" | "danger""default"Visual style of the dropdown item. - `"default"` — Standard item appearance - `"danger"` — Destructive action with red text

Button that opens the dropdown when clicked.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Individual menu item for actions.

PropTypeDefaultDescription
iconIcon | JSX.Element-Icon displayed before the label.
variant"default" | "danger""default"Visual style of the item.
selectedboolean-Shows a check mark indicator when true.
insetboolean-Adds left padding to align with items that have icons.
onClick(event: MouseEvent) => void-Callback when the item is clicked.
closeOnClickbooleantrueWhether the menu closes after clicking this item.
disabledboolean-When true, the item cannot be interacted with.

A menu item that navigates to a URL. Renders a semantic <a> element. Use this instead of Item for navigation links.

PropTypeDefaultDescription
hrefstring-URL to navigate to when clicked.
iconIcon | JSX.Element-Icon displayed before the label.
variant"default" | "danger""default"Visual style of the item.
insetboolean-Adds left padding to align with items that have icons.
targetstring-Link target attribute (e.g. "_blank" for new tab).
renderJSX.Element | ((props, state) => JSX.Element)-Custom element to render as the link. Use to integrate with framework routers (e.g. Next.js Link).

A menu item that can be toggled on or off. Use for independent boolean options.

PropTypeDefaultDescription
checkedboolean-Whether the item is checked.
defaultCheckedbooleanfalseWhether the item is initially checked (uncontrolled).
onCheckedChange(checked: boolean, event: ChangeEventDetails) => void-Callback when the checked state changes.
closeOnClickbooleanfalseWhether the menu closes after clicking this item.
disabledboolean-When true, the item cannot be interacted with.

Root component for a nested submenu. Wrap SubTrigger and SubContent inside this.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Menu item that opens a nested submenu when hovered or clicked. Displays a caret icon automatically.

PropTypeDefaultDescription
iconIcon-Icon displayed before the label.
insetboolean-Adds left padding to align with items that have icons.

Container for submenu items. Positioned relative to the SubTrigger.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

A visual divider between menu items.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Groups radio items for single-selection behavior. Only one item can be selected at a time.

PropTypeDefaultDescription
valueany-The controlled value of the currently selected radio item.
defaultValueany-The initially selected value (uncontrolled).
onValueChange(value: any, event: ChangeEventDetails) => void-Callback when the selected value changes.
disabledboolean-When true, all radio items in the group are disabled.

A menu item that works like a radio button. Must be used inside a RadioGroup.

PropTypeDefaultDescription
value*any-The value of this radio item.
iconIcon | JSX.Element-Icon displayed before the label.
insetboolean-Adds left padding to align with items that have icons.
closeOnClickbooleanfalseWhether the menu closes after clicking this item.
disabledboolean-When true, the item cannot be interacted with.

Shows the selected state for a RadioItem. Displays a checkmark by default.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.