COMPONENTS/OVERLAYS

Dropdown menu.

The right actions, within reach.

In practice

Compose DropdownMenuTrigger, Content and Item. Supports keyboard navigation and disabled items.

Installation

Install Coal once. Components and TypeScript declarations are included. React 19 and React DOM 19 are peer dependencies.

Terminal · local package
npm install http://localhost:3100/downloads/chlohal-coal-ui-0.5.0.tgz

This installs the local package. It is not yet published on npm. See installation for stylesheet setup and integration.

Usage

example.tsx
"use client";
import * as React from "react";
import "@chlohal/coal-ui/styles.css";
import { ChevronDown } from "lucide-react";
import { Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@chlohal/coal-ui";
export default function Example() {
    const [message, setMessage] = React.useState("");
    const notice = <p role="status">{message}</p>;
    return (<div>
          <DropdownMenu>
            <DropdownMenuTrigger render={<Button variant="outline"/>}>
              Actions <ChevronDown size={14}/>
            </DropdownMenuTrigger>
            <DropdownMenuContent>
              {["Edit project", "Duplicate", "Archive"].map((s) => (<DropdownMenuItem key={s} onClick={() => setMessage(s + " selected.")}>
                  {s}
                </DropdownMenuItem>))}
            </DropdownMenuContent>
          </DropdownMenu>
          {notice}
        </div>);
}

Source

The original Coal implementation. Use the package export to integrate it.

packages/react/src/dropdown-menu.tsx
"use client";
import * as React from "react";
import { Action, cn, moveFocus, type ActionProps } from "./internal.js";
import {
  FloatingTrigger,
  FloatingContent,
  useFloating,
  type FloatingContentProps,
} from "./floating.js";
export { FloatingRoot as DropdownMenu } from "./floating.js";
export function DropdownMenuTrigger(props: ActionProps) {
  return <FloatingTrigger aria-haspopup="menu" {...props} />;
}
export function DropdownMenuContent({
  onKeyDown,
  ...props
}: FloatingContentProps) {
  return (
    <FloatingContent
      role="menu"
      {...props}
      onKeyDown={(e) => {
        onKeyDown?.(e);
        if (!e.defaultPrevented) {
          moveFocus(e, '[role="menuitem"]');
          if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
            const item = Array.from(
              e.currentTarget.querySelectorAll<HTMLElement>(
                '[role="menuitem"]',
              ),
            ).find(
              (el) =>
                !el.hasAttribute("disabled") &&
                el.textContent?.toLowerCase().startsWith(e.key.toLowerCase()),
            );
            item?.focus();
          }
        }
      }}
    />
  );
}
export function DropdownMenuItem({
  className,
  onClick,
  ...props
}: ActionProps) {
  const c = useFloating();
  return (
    <Action
      role="menuitem"
      tabIndex={-1}
      className={cn("coal-menu-item", className)}
      {...props}
      onClick={(e) => {
        onClick?.(e);
        if (!e.defaultPrevented) c.close();
      }}
    />
  );
}
export function DropdownMenuSeparator(
  props: React.ComponentPropsWithRef<"hr">,
) {
  return <hr className="coal-separator" {...props} />;
}
export function DropdownMenuGroup(props: React.ComponentPropsWithRef<"div">) {
  return <div role="group" {...props} />;
}
export function DropdownMenuGroupLabel(
  props: React.ComponentPropsWithRef<"div">,
) {
  return <div className="coal-menu-label" {...props} />;
}

Accessibility checklist

  • Provide an accessible name for controls, including icon-only buttons.
  • Check keyboard navigation, visible focus and disabled states in your application.
  • Do not rely on color alone to communicate status.