Dropdown Menu
A keyboard-navigable menu anchored to a trigger.
- Chromium
- Firefox
- WebKit
- WCAG 2.2 AA
Preview
import { Button } from "@moe-ui/registry/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@moe-ui/registry/components/ui/dropdown-menu";
import { Text } from "@moe-ui/registry/components/ui/text";
export default function DropdownMenuPreview() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<Text>Open</Text>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Text>Profile</Text>
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<Text>Billing</Text>
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<Text>Settings</Text>
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<Text>Keyboard shortcuts</Text>
<DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Text>Team</Text>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Text>Invite users</Text>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>
<Text>Email</Text>
</DropdownMenuItem>
<DropdownMenuItem>
<Text>Message</Text>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Text>More...</Text>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuItem>
<Text>New Team</Text>
<DropdownMenuShortcut>⌘+T</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Text>GitHub</Text>
</DropdownMenuItem>
<DropdownMenuItem>
<Text>Support</Text>
</DropdownMenuItem>
<DropdownMenuItem disabled>
<Text>API</Text>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Text>Log out</Text>
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
One command, your source
Installation
pnpm dlx @moe-ui/cli@beta add dropdown-menuUsage
The CLI installs editable source into your application. Import the component from your configured UI directory:
import { DropdownMenu } from "@/components/ui/dropdown-menu";Variants and composition
Compound parts and variants remain regular source in your project, so you can change them without wrapping a package API.
Public API
The canonical source panel below lists every public export, dependency, and the complete implementation shipped by the registry.
Keyboard and accessibility
The trigger is keyboard reachable, Escape dismisses the surface, and focus returns to the trigger. Modal surfaces keep focus within the active layer.
Browser support
This beta is release-tested in Chromium, Firefox, and WebKit in light and dark themes. See the web compatibility matrix for the current gate.
Nothing hiddenCanonical source and public exports15 exports
DropdownMenuDropdownMenuCheckboxItemDropdownMenuContentDropdownMenuGroupDropdownMenuItemDropdownMenuLabelDropdownMenuPortalDropdownMenuRadioGroupDropdownMenuRadioItemDropdownMenuSeparatorDropdownMenuShortcutDropdownMenuSubDropdownMenuSubContentDropdownMenuSubTriggerDropdownMenuTrigger"use client";
import { Icon } from "./icon";
import { NativeOnlyAnimatedView } from "./native-only-animated-view";
import { TextClassContext } from "./text";
import { cn } from "../../lib/utils";
import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
import {
Check,
ChevronDown,
ChevronRight,
ChevronUp,
} from "lucide-react-native";
import * as React from "react";
import {
Platform,
type StyleProp,
StyleSheet,
Text,
type TextProps,
View,
type ViewStyle,
} from "react-native";
import { FadeIn } from "react-native-reanimated";
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
function DropdownMenuSubTrigger({
className,
inset,
children,
iconClassName,
...props
}: DropdownMenuPrimitive.SubTriggerProps &
React.RefAttributes<DropdownMenuPrimitive.SubTriggerRef> & {
children?: React.ReactNode;
iconClassName?: string;
inset?: boolean;
}) {
const { open } = DropdownMenuPrimitive.useSubContext();
const icon =
Platform.OS === "web" ? ChevronRight : open ? ChevronUp : ChevronDown;
return (
<TextClassContext.Provider
value={cn(
"text-sm select-none group-active:text-accent-foreground",
open && "text-accent-foreground",
)}
>
<DropdownMenuPrimitive.SubTrigger
className={cn(
"active:bg-accent group flex flex-row items-center justify-between rounded-sm px-2 py-2 sm:py-1.5",
Platform.select({
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none [&_svg]:pointer-events-none",
}),
open && "bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
<>{children}</>
<Icon
as={icon}
className={cn("text-foreground size-4 shrink-0", iconClassName)}
/>
</DropdownMenuPrimitive.SubTrigger>
</TextClassContext.Provider>
);
}
function DropdownMenuSubContent({
className,
...props
}: DropdownMenuPrimitive.SubContentProps &
React.RefAttributes<DropdownMenuPrimitive.SubContentRef>) {
return (
<NativeOnlyAnimatedView entering={FadeIn}>
<DropdownMenuPrimitive.SubContent
className={cn(
"bg-popover border-border overflow-hidden rounded-md border p-1 shadow-lg shadow-black/5",
Platform.select({
web: "animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 fade-in-0 data-[state=closed]:zoom-out-95 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[var(--radix-context-menu-content-transform-origin)] z-50 min-w-[8rem]",
}),
className,
)}
{...props}
/>
</NativeOnlyAnimatedView>
);
}
const FullWindowOverlay =
Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
function DropdownMenuContent({
className,
overlayClassName,
overlayStyle,
portalHost,
...props
}: DropdownMenuPrimitive.ContentProps &
React.RefAttributes<DropdownMenuPrimitive.ContentRef> & {
overlayStyle?: StyleProp<ViewStyle>;
overlayClassName?: string;
portalHost?: string;
}) {
return (
<DropdownMenuPrimitive.Portal hostName={portalHost}>
<FullWindowOverlay>
<DropdownMenuPrimitive.Overlay
style={Platform.select({
web: overlayStyle ?? undefined,
native: overlayStyle
? StyleSheet.flatten([
StyleSheet.absoluteFill,
overlayStyle as typeof StyleSheet.absoluteFill,
])
: StyleSheet.absoluteFill,
})}
className={overlayClassName}
>
<NativeOnlyAnimatedView entering={FadeIn}>
<TextClassContext.Provider value="text-popover-foreground">
<DropdownMenuPrimitive.Content
className={cn(
"bg-popover border-border min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg shadow-black/5",
Platform.select({
web: cn(
"animate-in fade-in-0 zoom-in-95 max-h-[var(--radix-context-menu-content-available-height)] origin-[var(--radix-context-menu-content-transform-origin)] z-50 cursor-default",
props.side === "bottom" && "slide-in-from-top-2",
props.side === "top" && "slide-in-from-bottom-2",
),
}),
className,
)}
{...props}
/>
</TextClassContext.Provider>
</NativeOnlyAnimatedView>
</DropdownMenuPrimitive.Overlay>
</FullWindowOverlay>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuItem({
className,
inset,
variant,
...props
}: DropdownMenuPrimitive.ItemProps &
React.RefAttributes<DropdownMenuPrimitive.ItemRef> & {
className?: string;
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<TextClassContext.Provider
value={cn(
"select-none text-sm text-popover-foreground group-active:text-popover-foreground",
variant === "destructive" &&
"text-destructive group-active:text-destructive",
)}
>
<DropdownMenuPrimitive.Item
className={cn(
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm px-2 py-2 sm:py-1.5",
Platform.select({
web: cn(
"focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-[disabled]:pointer-events-none",
variant === "destructive" &&
"focus:bg-destructive/10 dark:focus:bg-destructive/20",
),
}),
variant === "destructive" &&
"active:bg-destructive/10 dark:active:bg-destructive/20",
props.disabled && "opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
</TextClassContext.Provider>
);
}
function DropdownMenuCheckboxItem({
className,
children,
...props
}: DropdownMenuPrimitive.CheckboxItemProps &
React.RefAttributes<DropdownMenuPrimitive.CheckboxItemRef> & {
children?: React.ReactNode;
}) {
return (
<TextClassContext.Provider value="text-sm text-popover-foreground select-none group-active:text-accent-foreground">
<DropdownMenuPrimitive.CheckboxItem
className={cn(
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm py-2 pl-8 pr-2 sm:py-1.5",
Platform.select({
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-[disabled]:pointer-events-none",
}),
props.disabled && "opacity-50",
className,
)}
{...props}
>
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Icon
as={Check}
className={cn(
"text-foreground size-4",
Platform.select({ web: "pointer-events-none" }),
)}
/>
</DropdownMenuPrimitive.ItemIndicator>
</View>
<>{children}</>
</DropdownMenuPrimitive.CheckboxItem>
</TextClassContext.Provider>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: DropdownMenuPrimitive.RadioItemProps &
React.RefAttributes<DropdownMenuPrimitive.RadioItemRef> & {
children?: React.ReactNode;
}) {
return (
<TextClassContext.Provider value="text-sm text-popover-foreground select-none group-active:text-accent-foreground">
<DropdownMenuPrimitive.RadioItem
className={cn(
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm py-2 pl-8 pr-2 sm:py-1.5",
Platform.select({
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-[disabled]:pointer-events-none",
}),
props.disabled && "opacity-50",
className,
)}
{...props}
>
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<View className="bg-foreground h-2 w-2 rounded-full" />
</DropdownMenuPrimitive.ItemIndicator>
</View>
<>{children}</>
</DropdownMenuPrimitive.RadioItem>
</TextClassContext.Provider>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: DropdownMenuPrimitive.LabelProps &
React.RefAttributes<DropdownMenuPrimitive.LabelRef> & {
className?: string;
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
className={cn(
"text-foreground px-2 py-2 text-sm font-medium sm:py-1.5",
inset && "pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: DropdownMenuPrimitive.SeparatorProps &
React.RefAttributes<DropdownMenuPrimitive.SeparatorRef>) {
return (
<DropdownMenuPrimitive.Separator
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: TextProps & React.RefAttributes<Text>) {
return (
<Text
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
};