Aura Design System

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-menu

Manual

Install the following dependencies:

pnpm install @radix-ui/react-icons @radix-ui/react-slot

Copy 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>
)