- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
Installation
$ pnpm dlx shadcn@latest add @cobalt/dropdown-menu
Model picker
A radio group. The selected row keeps the hover fill and shows a radio glyph at the end of the row with indicator="radio".
Team menu
A header and a footer sit next to the groups as plain siblings. Rows can carry a trailing badge, and DropdownMenuSeparator bleeds through the panel padding.
Account menu
Row content is free-form, so rows can hold avatars instead of icons, and the panel can end with a divider and a pair of buttons.
Submenus, checkboxes and the compact size
Options
<DropdownMenuContent size="sm">tighter menu: 12px radius and 6px padding instead of 16px and 10px.<DropdownMenuRadioItem indicator="radio">shows a radio glyph instead of a check.
Source
"use client";
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { RiArrowRightSLine, RiCheckLine } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
}
function DropdownMenuTrigger({
className,
surface = "none",
...props
}: MenuPrimitive.Trigger.Props & {
/** `tile` is a filled 12px row (account switchers): hairline on hover, hairline and a darker fill while open. */
surface?: "none" | "tile";
}) {
return (
<MenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
className={
surface === "tile"
? cn(
"overflow-hidden rounded-[12px] border border-transparent bg-muted p-2.25 transition-colors duration-150 hover:border-[var(--border-hover,var(--border))] hover:bg-[var(--surface-strong,var(--accent))] data-popup-open:border-border data-popup-open:bg-[var(--surface-strong,var(--accent))] motion-reduce:transition-none",
className as string | undefined
)
: className
}
{...props}
/>
);
}
function DropdownMenuContent({
align = "start",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
collisionPadding = 8,
size = "default",
className,
...props
}: MenuPrimitive.Popup.Props &
Pick<
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "collisionPadding"
> & { size?: "default" | "sm" }) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
collisionPadding={collisionPadding}
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<MenuPrimitive.Popup
data-slot="dropdown-menu-content"
data-size={size}
className={cn(
"group/dropdown-menu-content flex max-h-(--available-height) w-[266px] max-w-[calc(100vw-32px)] origin-(--transform-origin) flex-col gap-1 overflow-x-hidden overflow-y-auto rounded-[16px] border border-[var(--border-subtle,var(--border))] bg-[var(--surface-raised,var(--popover))] dark:border-border p-2.5 text-popover-foreground shadow-[shadow:var(--shadow-dropdown,0_1px_1px_0_rgb(0_0_0/0.04),0_4px_4px_0_rgb(0_0_0/0.02))] outline-none transition-[opacity,scale,filter] duration-150 ease-out data-starting-style:scale-95 data-starting-style:opacity-0 data-starting-style:blur-[2px] data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:blur-[2px] motion-reduce:transition-none motion-reduce:data-starting-style:scale-100 motion-reduce:data-starting-style:opacity-100 motion-reduce:data-starting-style:blur-none motion-reduce:data-ending-style:scale-100 motion-reduce:data-ending-style:opacity-100 motion-reduce:data-ending-style:blur-none",
size === "sm" && "rounded-[12px] p-1.5",
className
)}
{...props}
/>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
);
}
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return (
<MenuPrimitive.Group
data-slot="dropdown-menu-group"
className="flex w-full flex-col gap-1"
{...props}
/>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: MenuPrimitive.GroupLabel.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 pt-1 pb-0.5 text-sm leading-5 font-medium text-muted-foreground data-inset:pl-8",
className
)}
{...props}
/>
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: MenuPrimitive.Item.Props & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex w-full cursor-pointer items-center gap-2 rounded-[10px] p-2 text-start text-sm leading-5 font-medium outline-none select-none transition-colors group-data-ending-style/dropdown-menu-content:delay-150 motion-reduce:transition-none data-highlighted:bg-[var(--surface-hover,var(--accent))] data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5 [&_svg:not([class*='text-'])]:text-[color:var(--icon-foreground,var(--muted-foreground))] data-[variant=destructive]:[&_svg:not([class*='text-'])]:text-destructive",
className
)}
{...props}
/>
);
}
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: MenuPrimitive.SubmenuTrigger.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex w-full cursor-pointer items-center gap-2 rounded-[10px] p-2 text-start text-sm leading-5 font-medium outline-none select-none transition-colors group-data-ending-style/dropdown-menu-content:delay-150 motion-reduce:transition-none data-highlighted:bg-[var(--surface-hover,var(--accent))] data-popup-open:bg-[var(--surface-hover,var(--accent))] data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5 [&_svg:not([class*='text-'])]:text-[color:var(--icon-foreground,var(--muted-foreground))] data-[variant=destructive]:[&_svg:not([class*='text-'])]:text-destructive",
className
)}
{...props}
>
{children}
<RiArrowRightSLine className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
);
}
function DropdownMenuSubContent({
align = "start",
alignOffset = -3,
side = "right",
sideOffset = 0,
className,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
className={cn("w-[266px]", className)}
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: MenuPrimitive.CheckboxItem.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex w-full cursor-pointer items-center gap-2 rounded-[10px] p-2 pr-8 text-start text-sm leading-5 font-medium outline-none select-none transition-colors group-data-ending-style/dropdown-menu-content:delay-150 motion-reduce:transition-none isolate before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] data-checked:transition-none data-checked:not-data-highlighted:before:bg-[var(--surface-hover,var(--accent))] data-highlighted:bg-[var(--surface-hover,var(--accent))] data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<MenuPrimitive.CheckboxItemIndicator>
<RiCheckLine />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
className,
...props
}: MenuPrimitive.RadioGroup.Props) {
return (
<MenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
className={cn("flex w-full flex-col gap-1", className)}
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
inset,
indicator = "check",
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean;
indicator?: "check" | "radio";
}) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"group/dropdown-menu-radio-item relative flex w-full cursor-pointer items-center gap-2 rounded-[10px] p-2 pr-8 text-start text-sm leading-5 font-medium outline-none select-none transition-colors group-data-ending-style/dropdown-menu-content:delay-150 motion-reduce:transition-none isolate before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] data-checked:transition-none data-checked:not-data-highlighted:before:bg-[var(--surface-hover,var(--accent))] data-highlighted:bg-[var(--surface-hover,var(--accent))] data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{indicator === "radio" ? (
<span
className="pointer-events-none absolute right-2 flex size-4 items-center justify-center rounded-full border border-[var(--border-hover,var(--border))] bg-popover transition-colors group-data-checked/dropdown-menu-radio-item:border-primary group-data-checked/dropdown-menu-radio-item:bg-primary motion-reduce:transition-none"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator className="size-1.5 rounded-full bg-primary-foreground" />
</span>
) : (
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<MenuPrimitive.RadioItemIndicator>
<RiCheckLine />
</MenuPrimitive.RadioItemIndicator>
</span>
)}
{children}
</MenuPrimitive.RadioItem>
);
}
function DropdownMenuSeparator({
className,
...props
}: MenuPrimitive.Separator.Props) {
return (
<MenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn(
"-mx-2.5 my-1.5 h-px shrink-0 bg-[var(--border-subtle,var(--border))] dark:bg-border",
className
)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
className
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};Radio and checkbox items
DropdownMenuRadioItem and DropdownMenuCheckboxItem keep the menu open after a pick. This is the Base UI default, so you can change several options in one visit. For a single choice that should close the menu, pass closeOnClick to the item.