docs / core / dropdown-menu
DropdownMenu
a list of actions anchored to whatever triggered them. it portals above everything and hands the arrow keys the list. actions, not navigation.
intensity · recedesrsc · client
provenance
authored in usvalayer
core / primitivesintensity
recedes · safe anywhere, including dense surfacescomposition
hangs off an icon Button in a Toolbar, a table row's overflow, a card cornerSeparator groups items, Label names a groupnavigation goes in a link, never in a menu itemno inputs or forms inside. that is a Popovera11y
menu semantics from Base UI · arrow keys rove, Enter selects, Escape closes · disabled items carrydata-disabledjest-axedependencies
@base-ui/reactlive demo · try it out
customize
triggerthe button text
Labelgroup name above items, empty hides it
sideOffsetgap between trigger and panel
disableddims the delete item, blocks selection
usage
import { DropdownMenu } from "usva/primitives/dropdown-menu";
<DropdownMenu>
<DropdownMenu.Trigger>Open menu</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Actions</DropdownMenu.Label>
<DropdownMenu.Item>Edit</DropdownMenu.Item>
<DropdownMenu.Item>Duplicate</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item disabled>Delete</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>props
| prop | type | default | notes |
|---|---|---|---|
| open | boolean | — | controlled open state. |
| defaultOpen | boolean | false | initial open state when uncontrolled. |
| onOpenChange | (open, details) => void | — | fires when the menu opens or closes. |
| Content sideOffset | number | 6 | gap between the trigger and the panel. |
| Item onSelect | (event) => void | — | fires when the item is picked, by click or Enter. |
| Item disabled | boolean | false | dims the item and blocks selection. |
get it
npx shadcn add https://usva.build/r/dropdown-menu.jsonsource · components/ui/dropdown-menu.tsxexactly what this command copies
"use client";
import { Menu as Base } from "@base-ui/react/menu";
import * as React from "react";
import { cn } from "@/lib/utils";
import { useScopedTheme } from "./use-scoped-theme";
export type DropdownMenuProps = Base.Root.Props;
function DropdownMenuRoot(props: DropdownMenuProps) {
return <Base.Root {...props} />;
}
DropdownMenuRoot.displayName = "DropdownMenu";
export const DropdownMenuTrigger = Base.Trigger;
export type DropdownMenuContentProps = React.ComponentPropsWithoutRef<
typeof Base.Popup
> & {
sideOffset?: number;
};
export const DropdownMenuContent = React.forwardRef<
HTMLDivElement,
DropdownMenuContentProps
>(({ className, sideOffset = 6, children, ...props }, ref) => {
const [probe, theme] = useScopedTheme();
return (
<>
<span ref={probe} hidden />
<Base.Portal>
<Base.Positioner
data-theme={theme}
sideOffset={sideOffset}
className="z-dropdown"
>
<Base.Popup
ref={ref}
className={cn(
"rim-light min-w-40 rounded-lg border border-border bg-surface-2 p-1 text-sm text-ink shadow-floating",
"origin-[var(--transform-origin)] transition-enter duration-base ease-spring motion-reduce:transition-none motion-reduce:transform-none",
"data-[starting-style]:translate-y-1 data-[starting-style]:scale-[0.97] data-[starting-style]:opacity-0 data-[starting-style]:blur-[2px]",
"data-[ending-style]:scale-[0.98] data-[ending-style]:opacity-0 data-[ending-style]:duration-fast data-[ending-style]:ease-soft",
className,
)}
{...props}
>
{children}
</Base.Popup>
</Base.Positioner>
</Base.Portal>
</>
);
});
DropdownMenuContent.displayName = "DropdownMenuContent";
export type DropdownMenuItemProps = Omit<
React.ComponentPropsWithoutRef<typeof Base.Item>,
"onClick"
> & {
onSelect?: React.ComponentPropsWithoutRef<typeof Base.Item>["onClick"];
};
export const DropdownMenuItem = React.forwardRef<
HTMLDivElement,
DropdownMenuItemProps
>(({ className, onSelect, ...props }, ref) => (
<Base.Item
ref={ref}
onClick={onSelect}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 text-muted outline-none",
"before:absolute before:inset-0 before:rounded-[inherit] before:bg-ink/0 before:transition-tint before:duration-fast before:content-[''] motion-reduce:before:transition-none",
"[&>*]:relative data-[highlighted]:text-ink data-[highlighted]:before:bg-ink/5",
"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = "DropdownMenuItem";
export const DropdownMenuSeparator = React.forwardRef<
HTMLHRElement,
React.ComponentPropsWithoutRef<"hr">
>(({ className, ...props }, ref) => (
<hr
ref={ref}
className={cn("-mx-1 my-1 h-px border-0 bg-border", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
export const DropdownMenuLabel = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("px-2.5 py-1.5 text-xs text-muted", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = "DropdownMenuLabel";
export const DropdownMenu = Object.assign(DropdownMenuRoot, {
Trigger: DropdownMenuTrigger,
Content: DropdownMenuContent,
Item: DropdownMenuItem,
Separator: DropdownMenuSeparator,
Label: DropdownMenuLabel,
});