Responsive Dropdown Menu
A dropdown menu on desktop and a stacked drawer for nested menus on mobile.
Preview
import { Button } from "@/components/ui/Button";
import {
ResponsiveDropdownMenu,
ResponsiveDropdownMenuContent,
ResponsiveDropdownMenuGroup,
ResponsiveDropdownMenuItem,
ResponsiveDropdownMenuLabel,
ResponsiveDropdownMenuSeparator,
ResponsiveDropdownMenuShortcut,
ResponsiveDropdownMenuSub,
ResponsiveDropdownMenuSubContent,
ResponsiveDropdownMenuSubTrigger,
ResponsiveDropdownMenuTrigger,
} from "@/components/ui/ResponsiveDropdownMenu";
export function ResponsiveDropdownMenuDemo() {
return (
<ResponsiveDropdownMenu>
<ResponsiveDropdownMenuTrigger asChild>
<Button variant="menu">Open menu</Button>
</ResponsiveDropdownMenuTrigger>
<ResponsiveDropdownMenuContent
mobileTitle="Account"
className="data-[variant=dropdown]:w-20"
>
<ResponsiveDropdownMenuLabel>My account</ResponsiveDropdownMenuLabel>
<ResponsiveDropdownMenuSeparator />
<ResponsiveDropdownMenuGroup>
<ResponsiveDropdownMenuItem>
Profile
<ResponsiveDropdownMenuShortcut>⇧⌘P</ResponsiveDropdownMenuShortcut>
</ResponsiveDropdownMenuItem>
<ResponsiveDropdownMenuItem>Notifications</ResponsiveDropdownMenuItem>
<ResponsiveDropdownMenuSub>
<ResponsiveDropdownMenuSubTrigger>
Share
</ResponsiveDropdownMenuSubTrigger>
<ResponsiveDropdownMenuSubContent>
<ResponsiveDropdownMenuItem>Copy link</ResponsiveDropdownMenuItem>
<ResponsiveDropdownMenuSub>
<ResponsiveDropdownMenuSubTrigger>
Send to team
</ResponsiveDropdownMenuSubTrigger>
<ResponsiveDropdownMenuSubContent>
<ResponsiveDropdownMenuItem>Design</ResponsiveDropdownMenuItem>
<ResponsiveDropdownMenuItem>
Engineering
</ResponsiveDropdownMenuItem>
</ResponsiveDropdownMenuSubContent>
</ResponsiveDropdownMenuSub>
</ResponsiveDropdownMenuSubContent>
</ResponsiveDropdownMenuSub>
</ResponsiveDropdownMenuGroup>
<ResponsiveDropdownMenuSeparator />
<ResponsiveDropdownMenuItem>Log out</ResponsiveDropdownMenuItem>
</ResponsiveDropdownMenuContent>
</ResponsiveDropdownMenu>
)
}Installation
Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces
pnpm dlx shadcn@latest add @aura/responsive-dropdown-menuManual
Install the following dependencies:
pnpm install @radix-ui/react-icons @radix-ui/react-slotCopy and paste the class names utility into your utils/class-names.ts file.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy and paste the drawer component into your components/ui/Drawer.tsx file.
"use client";
/**
* @description A drawer component that slides in from the edge of the screen.
*/
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/utils/class-names";
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn("fixed inset-0 z-50 bg-gray-9a backdrop-blur-xs", className)}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-8 flex h-auto flex-col rounded-t-[10px] border border-gray-6 bg-gray-1",
className
)}
{...props}
>
<div className="mx-auto mt-1 h-0.5 w-8 rounded-full bg-gray-3" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("grid gap-0.5 p-1.5 text-center sm:text-left", className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-1 p-1.5", className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn(
"text-lg font-semibold leading-none tracking-tight text-gray-12",
className
)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-sm text-gray-11", className)}
{...props}
/>
);
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};
Copy and paste the dropdown menu component into your components/ui/DropdownMenu.tsx file.
"use client";
/**
* @description Displays a menu of actions or options triggered by a button.
*/
import * as React from "react";
import { DropdownMenu as DropdownMenuRadix } from "radix-ui";
import {
CheckIcon,
ChevronRightIcon,
CircleIcon,
DotFilledIcon,
} from "@radix-ui/react-icons";
import { cn } from "@/utils/class-names";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Root>) {
return <DropdownMenuRadix.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Portal>) {
return (
<DropdownMenuRadix.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Trigger>) {
return (
<DropdownMenuRadix.Trigger
data-slot="dropdown-menu-trigger"
className={cn(className)}
{...props}
/>
);
}
function DropdownMenuContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Content>) {
return (
<DropdownMenuRadix.Portal>
<DropdownMenuRadix.Content
data-slot="dropdown-menu-content"
collisionPadding={8}
className={cn(
className,
"z-50 origin-(--radix-dropdown-menu-content-transform-origin) bg-gray-1 border border-gray-a6 rounded-sm relative shadow-md data-[state=open]:animate-popover-show data-[state=closed]:animate-popover-hide"
)}
{...props}
/>
</DropdownMenuRadix.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Group>) {
return <DropdownMenuRadix.Group data-slot="dropdown-menu-group" {...props} />;
}
function DropdownMenuItem({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Item>) {
return (
<DropdownMenuRadix.Item
data-slot="dropdown-menu-item"
className={cn(
className,
"p-0.5 px-2 hover:bg-accent-3 flex relative cursor-pointer"
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
children,
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.CheckboxItem>) {
return (
<DropdownMenuRadix.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
className,
"p-0.5 px-2 hover:bg-accent-3 flex relative cursor-pointer"
)}
{...props}
>
<span className="absolute left-0.5 top-0 bottom-0 items-center flex justify-center">
<DropdownMenuRadix.ItemIndicator>
<CheckIcon className="text-accent-9" />
</DropdownMenuRadix.ItemIndicator>
</span>
{children}
</DropdownMenuRadix.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.RadioGroup>) {
return (
<DropdownMenuRadix.RadioGroup
data-slot="dropdown-menu-radio-group"
className={cn(className)}
{...props}
/>
);
}
function DropdownMenuRadioItem({
children,
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.RadioItem>) {
return (
<DropdownMenuRadix.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
className,
"p-0.5 px-2 hover:bg-accent-3 flex relative cursor-pointer"
)}
{...props}
>
<span className="absolute left-0.5 top-0 bottom-0 items-center flex justify-center">
<DropdownMenuRadix.ItemIndicator>
<DotFilledIcon className="fill-current text-accent-9" />
</DropdownMenuRadix.ItemIndicator>
</span>
{children}
</DropdownMenuRadix.RadioItem>
);
}
function DropdownMenuLabel({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Label>) {
return (
<DropdownMenuRadix.Label
data-slot="dropdown-menu-label"
className={cn(className, "p-0.5 px-2 text-gray-12")}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Separator>) {
return (
<DropdownMenuRadix.Separator
data-slot="dropdown-menu-separator"
className={cn(className, "m-0.6 h-px bg-gray-a6")}
{...props}
/>
);
}
function DropdownMenuShortcut({ ...props }: React.ComponentProps<"span">) {
return <span data-slot="dropdown-menu-shortcut" {...props} />;
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuRadix.Sub>) {
return <DropdownMenuRadix.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
children,
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.SubTrigger>) {
return (
<DropdownMenuRadix.SubTrigger
data-slot="dropdown-menu-sub-trigger"
className={cn(
className,
"p-0.5 px-2 hover:bg-accent-3 flex relative cursor-pointer"
)}
{...props}
>
{children}
<div className="absolute right-0.5 top-0 bottom-0 items-center flex justify-center">
<ChevronRightIcon />
</div>
</DropdownMenuRadix.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadix.SubContent>) {
return (
<DropdownMenuRadix.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
className,
"z-50 origin-(--radix-dropdown-menu-content-transform-origin) bg-gray-1 border border-gray-a6 rounded-sm relative shadow-md data-[state=open]:animate-popover-show data-[state=closed]:animate-popover-hide"
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};
Copy and paste the use mobile hook into your hooks/use-mobile.ts file.
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile(breakpoint = MOBILE_BREAKPOINT) {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < breakpoint)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < breakpoint)
return () => mql.removeEventListener("change", onChange)
}, [breakpoint])
return !!isMobile
}
Copy and paste the ResponsiveDropdownMenu component into your components/ui/ResponsiveDropdownMenu.tsx file.
"use client";
/**
* @description A dropdown menu on desktop and a stacked drawer on mobile.
*/
import * as React from "react";
import { createPortal } from "react-dom";
import { Slot } from "@radix-ui/react-slot";
import {
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
DotFilledIcon,
} from "@radix-ui/react-icons";
import { cn } from "@/utils/class-names";
import { useIsMobile } from "@/hooks/use-mobile";
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/Drawer";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/DropdownMenu";
type StackEntry = {
id: string;
title: React.ReactNode;
};
type ResponsiveDropdownMenuContextValue = {
isMobile: boolean;
open: boolean;
setOpen: (open: boolean) => void;
stack: StackEntry[];
push: (entry: StackEntry) => void;
pop: () => void;
portalContainer: HTMLDivElement | null;
setPortalContainer: (container: HTMLDivElement | null) => void;
};
const ResponsiveDropdownMenuContext =
React.createContext<ResponsiveDropdownMenuContextValue | null>(null);
const MobilePanelDepthContext = React.createContext(0);
const SubContext = React.createContext<{ id: string; depth: number } | null>(
null,
);
const RadioGroupContext = React.createContext<{
value?: string;
onValueChange?: (value: string) => void;
} | null>(null);
function useResponsiveDropdownMenu() {
const context = React.useContext(ResponsiveDropdownMenuContext);
if (!context) {
throw new Error(
"`useResponsiveDropdownMenu` must be used within `ResponsiveDropdownMenu`",
);
}
return context;
}
interface ResponsiveDropdownMenuProps
extends React.ComponentProps<typeof DropdownMenu> {
breakpoint?: number;
}
function ResponsiveDropdownMenu({
breakpoint = 768,
open: openProp,
defaultOpen = false,
onOpenChange,
children,
modal,
...props
}: ResponsiveDropdownMenuProps) {
const isMobile = useIsMobile(breakpoint);
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const [stack, setStack] = React.useState<StackEntry[]>([]);
const [portalContainer, setPortalContainer] =
React.useState<HTMLDivElement | null>(null);
const open = openProp ?? internalOpen;
const setOpen = React.useCallback(
(nextOpen: boolean) => {
if (openProp === undefined) setInternalOpen(nextOpen);
if (!nextOpen) setStack([]);
onOpenChange?.(nextOpen);
},
[onOpenChange, openProp],
);
const push = React.useCallback((entry: StackEntry) => {
setStack((current) => {
const existingIndex = current.findIndex(({ id }) => id === entry.id);
if (existingIndex >= 0) return current.slice(0, existingIndex + 1);
return [...current, entry];
});
}, []);
const pop = React.useCallback(() => {
setStack((current) => current.slice(0, -1));
}, []);
const context = React.useMemo(
() => ({
isMobile,
open,
setOpen,
stack,
push,
pop,
portalContainer,
setPortalContainer,
}),
[isMobile, open, pop, portalContainer, push, setOpen, stack],
);
const root = isMobile ? (
<Drawer open={open} onOpenChange={setOpen} modal={modal}>
{children}
</Drawer>
) : (
<DropdownMenu open={open} onOpenChange={setOpen} modal={modal} {...props}>
{children}
</DropdownMenu>
);
return (
<ResponsiveDropdownMenuContext.Provider value={context}>
{root}
</ResponsiveDropdownMenuContext.Provider>
);
}
function ResponsiveDropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuTrigger>) {
const { isMobile } = useResponsiveDropdownMenu();
if (isMobile) {
return <DrawerTrigger data-variant="drawer" {...props} />;
}
return <DropdownMenuTrigger data-variant="dropdown" {...props} />;
}
function ResponsiveDropdownMenuPortal({
children,
...props
}: React.ComponentProps<typeof DropdownMenuPortal>) {
const { isMobile } = useResponsiveDropdownMenu();
if (isMobile) return <>{children}</>;
return <DropdownMenuPortal {...props}>{children}</DropdownMenuPortal>;
}
interface ResponsiveDropdownMenuContentProps
extends React.ComponentProps<typeof DropdownMenuContent> {
mobileTitle?: React.ReactNode;
}
function ResponsiveDropdownMenuContent({
className,
children,
mobileTitle = "Menu",
...props
}: ResponsiveDropdownMenuContentProps) {
const { isMobile, stack, pop, setPortalContainer } =
useResponsiveDropdownMenu();
if (!isMobile) {
return (
<DropdownMenuContent
data-variant="dropdown"
className={className}
{...props}
>
{children}
</DropdownMenuContent>
);
}
const currentTitle = stack[stack.length - 1]?.title ?? mobileTitle;
return (
<DrawerContent
data-variant="drawer"
className={cn("responsive-dropdown-drawer", className)}
>
<DrawerHeader className="responsive-dropdown-header">
{stack.length > 0 ? (
<button
type="button"
className="responsive-dropdown-back"
onClick={pop}
aria-label="Back"
>
<ChevronLeftIcon className="icon" />
</button>
) : null}
<DrawerTitle className="responsive-dropdown-title">
{currentTitle}
</DrawerTitle>
</DrawerHeader>
<div
ref={setPortalContainer}
className="responsive-dropdown-stack"
data-depth={stack.length}
>
<MobilePanelDepthContext.Provider value={0}>
<div
className="responsive-dropdown-panel"
data-panel-state={stack.length === 0 ? "current" : "previous"}
aria-hidden={stack.length > 0}
>
{children}
</div>
</MobilePanelDepthContext.Provider>
</div>
</DrawerContent>
);
}
function ResponsiveDropdownMenuGroup({
className,
...props
}: React.ComponentProps<typeof DropdownMenuGroup>) {
const { isMobile } = useResponsiveDropdownMenu();
if (isMobile) {
return (
<div
role="group"
data-slot="responsive-dropdown-menu-group"
className={className}
{...props}
/>
);
}
return <DropdownMenuGroup className={className} {...props} />;
}
function ResponsiveDropdownMenuItem({
asChild,
children,
className,
disabled,
onClick,
onSelect,
...props
}: React.ComponentProps<typeof DropdownMenuItem>) {
const { isMobile, setOpen } = useResponsiveDropdownMenu();
if (!isMobile) {
return (
<DropdownMenuItem
asChild={asChild}
className={className}
disabled={disabled}
onClick={onClick}
onSelect={onSelect}
{...props}
>
{children}
</DropdownMenuItem>
);
}
const Comp = asChild ? Slot : "button";
return (
<Comp
{...(props as React.ComponentPropsWithoutRef<"button">)}
type={asChild ? undefined : "button"}
role="menuitem"
disabled={asChild ? undefined : disabled}
data-disabled={disabled ? "" : undefined}
className={cn("responsive-dropdown-item", className)}
onClick={(event) => {
onClick?.(event as never);
onSelect?.(event.nativeEvent);
if (!event.defaultPrevented && !disabled) setOpen(false);
}}
>
{children}
</Comp>
);
}
function ResponsiveDropdownMenuCheckboxItem({
checked,
children,
className,
disabled,
onCheckedChange,
onSelect,
...props
}: React.ComponentProps<typeof DropdownMenuCheckboxItem>) {
const { isMobile } = useResponsiveDropdownMenu();
if (!isMobile) {
return (
<DropdownMenuCheckboxItem
checked={checked}
className={className}
disabled={disabled}
onCheckedChange={onCheckedChange}
onSelect={onSelect}
{...props}
>
{children}
</DropdownMenuCheckboxItem>
);
}
const isChecked = checked === true;
return (
<button
{...(props as React.ComponentPropsWithoutRef<"button">)}
type="button"
role="menuitemcheckbox"
aria-checked={isChecked}
disabled={disabled}
className={cn("responsive-dropdown-item", className)}
onClick={(event) => {
onSelect?.(event.nativeEvent);
if (!event.defaultPrevented && !disabled) {
onCheckedChange?.(!isChecked);
}
}}
>
<span className="responsive-dropdown-indicator" aria-hidden="true">
{isChecked ? <CheckIcon className="icon" /> : null}
</span>
{children}
</button>
);
}
function ResponsiveDropdownMenuRadioGroup({
value,
onValueChange,
children,
className,
...props
}: React.ComponentProps<typeof DropdownMenuRadioGroup>) {
const { isMobile } = useResponsiveDropdownMenu();
if (!isMobile) {
return (
<DropdownMenuRadioGroup
value={value}
onValueChange={onValueChange}
className={className}
{...props}
>
{children}
</DropdownMenuRadioGroup>
);
}
return (
<RadioGroupContext.Provider value={{ value, onValueChange }}>
<div
role="group"
className={className}
{...(props as React.ComponentPropsWithoutRef<"div">)}
>
{children}
</div>
</RadioGroupContext.Provider>
);
}
function ResponsiveDropdownMenuRadioItem({
value,
children,
className,
disabled,
onSelect,
...props
}: React.ComponentProps<typeof DropdownMenuRadioItem>) {
const { isMobile } = useResponsiveDropdownMenu();
const group = React.useContext(RadioGroupContext);
if (!isMobile) {
return (
<DropdownMenuRadioItem
value={value}
className={className}
disabled={disabled}
onSelect={onSelect}
{...props}
>
{children}
</DropdownMenuRadioItem>
);
}
const isChecked = group?.value === value;
return (
<button
{...(props as React.ComponentPropsWithoutRef<"button">)}
type="button"
role="menuitemradio"
aria-checked={isChecked}
disabled={disabled}
className={cn("responsive-dropdown-item", className)}
onClick={(event) => {
onSelect?.(event.nativeEvent);
if (!event.defaultPrevented && !disabled) group?.onValueChange?.(value);
}}
>
<span className="responsive-dropdown-indicator" aria-hidden="true">
{isChecked ? <DotFilledIcon className="icon" /> : null}
</span>
{children}
</button>
);
}
function ResponsiveDropdownMenuLabel({
className,
...props
}: React.ComponentProps<typeof DropdownMenuLabel>) {
const { isMobile } = useResponsiveDropdownMenu();
if (isMobile) {
return (
<div
data-slot="responsive-dropdown-menu-label"
className={cn("responsive-dropdown-label", className)}
{...props}
/>
);
}
return <DropdownMenuLabel className={className} {...props} />;
}
function ResponsiveDropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuSeparator>) {
const { isMobile } = useResponsiveDropdownMenu();
if (isMobile) {
return (
<div
role="separator"
className={cn("responsive-dropdown-separator", className)}
{...props}
/>
);
}
return <DropdownMenuSeparator className={className} {...props} />;
}
function ResponsiveDropdownMenuShortcut({
className,
...props
}: React.ComponentProps<typeof DropdownMenuShortcut>) {
const { isMobile } = useResponsiveDropdownMenu();
return (
<DropdownMenuShortcut
aria-hidden={isMobile || undefined}
className={cn(isMobile && "responsive-dropdown-shortcut", className)}
{...props}
/>
);
}
function ResponsiveDropdownMenuSub({
children,
...props
}: React.ComponentProps<typeof DropdownMenuSub>) {
const { isMobile } = useResponsiveDropdownMenu();
const parentDepth = React.useContext(MobilePanelDepthContext);
const id = React.useId();
if (!isMobile) {
return <DropdownMenuSub {...props}>{children}</DropdownMenuSub>;
}
return (
<SubContext.Provider value={{ id, depth: parentDepth + 1 }}>
{children}
</SubContext.Provider>
);
}
function ResponsiveDropdownMenuSubTrigger({
asChild,
children,
className,
disabled,
...props
}: React.ComponentProps<typeof DropdownMenuSubTrigger>) {
const { isMobile, push } = useResponsiveDropdownMenu();
const sub = React.useContext(SubContext);
if (!isMobile) {
return (
<DropdownMenuSubTrigger
className={className}
disabled={disabled}
{...props}
>
{children}
</DropdownMenuSubTrigger>
);
}
if (!sub) {
throw new Error(
"`ResponsiveDropdownMenuSubTrigger` must be used within `ResponsiveDropdownMenuSub`",
);
}
const title = React.isValidElement<{ children?: React.ReactNode }>(children)
? children.props.children
: children;
if (asChild && React.isValidElement<{ children?: React.ReactNode }>(children)) {
return (
<Slot
{...(props as React.ComponentPropsWithoutRef<typeof Slot>)}
role="menuitem"
data-disabled={disabled ? "" : undefined}
className={cn("responsive-dropdown-item", className)}
onClick={() => {
if (!disabled) push({ id: sub.id, title });
}}
>
{React.cloneElement(
children,
undefined,
<>
{children.props.children}
<ChevronRightIcon className="icon responsive-dropdown-chevron" />
</>
)}
</Slot>
);
}
return (
<button
{...(props as React.ComponentPropsWithoutRef<"button">)}
type="button"
role="menuitem"
disabled={disabled}
data-disabled={disabled ? "" : undefined}
className={cn("responsive-dropdown-item", className)}
onClick={() => push({ id: sub.id, title })}
>
{children}
<ChevronRightIcon className="icon responsive-dropdown-chevron" />
</button>
);
}
function ResponsiveDropdownMenuSubContent({
children,
className,
...props
}: React.ComponentProps<typeof DropdownMenuSubContent>) {
const { isMobile, portalContainer, stack } = useResponsiveDropdownMenu();
const sub = React.useContext(SubContext);
if (!isMobile) {
return (
<DropdownMenuSubContent className={className} {...props}>
{children}
</DropdownMenuSubContent>
);
}
if (!sub || !portalContainer) return null;
const stackIndex = stack.findIndex(({ id }) => id === sub.id);
const panelState =
stackIndex === -1
? "next"
: stackIndex === stack.length - 1
? "current"
: "previous";
return createPortal(
<MobilePanelDepthContext.Provider value={sub.depth}>
<div
role="menu"
className={cn("responsive-dropdown-panel", className)}
data-panel-state={panelState}
aria-hidden={panelState !== "current"}
{...(props as React.ComponentPropsWithoutRef<"div">)}
>
{children}
</div>
</MobilePanelDepthContext.Provider>,
portalContainer,
);
}
export {
ResponsiveDropdownMenu,
ResponsiveDropdownMenuCheckboxItem,
ResponsiveDropdownMenuContent,
ResponsiveDropdownMenuGroup,
ResponsiveDropdownMenuItem,
ResponsiveDropdownMenuLabel,
ResponsiveDropdownMenuPortal,
ResponsiveDropdownMenuRadioGroup,
ResponsiveDropdownMenuRadioItem,
ResponsiveDropdownMenuSeparator,
ResponsiveDropdownMenuShortcut,
ResponsiveDropdownMenuSub,
ResponsiveDropdownMenuSubContent,
ResponsiveDropdownMenuSubTrigger,
ResponsiveDropdownMenuTrigger,
};
Usage
CustomBreakpoint
export const CustomBreakpoint = () => (
<ResponsiveDropdownMenu breakpoint={1024}>
<ResponsiveDropdownMenuTrigger asChild>
<Button variant="menu">Breakpoint 1024</Button>
</ResponsiveDropdownMenuTrigger>
<ResponsiveDropdownMenuContent mobileTitle="Project actions">
<ResponsiveDropdownMenuItem>Rename</ResponsiveDropdownMenuItem>
<ResponsiveDropdownMenuItem>Duplicate</ResponsiveDropdownMenuItem>
<ResponsiveDropdownMenuItem>Archive</ResponsiveDropdownMenuItem>
</ResponsiveDropdownMenuContent>
</ResponsiveDropdownMenu>
)