# iOS Menu Button

> iOS pull-down menu button with a frosted menu, destructive actions, and Esc / outside-click dismissal.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/ios-menu-button
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/ios-menu-button.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/ios-menu-button-demo.tsx"
import { Copy, Share, Trash2 } from "lucide-react"

import { IOSMenuButton } from "@/components/ios-menu-button"

export default function IOSMenuButtonDemo() {
  return (
    <div className="flex min-h-64 w-full max-w-md items-start justify-center rounded-2xl bg-neutral-100 p-10 dark:bg-neutral-900">
      <IOSMenuButton
        label="Options"
        items={[
          { label: "Copy", icon: <Copy /> },
          { label: "Share", icon: <Share /> },
          { label: "Delete", icon: <Trash2 />, destructive: true },
        ]}
      />
    </div>
  )
}
```

## Source

```tsx title="components/ios-menu-button.tsx"
"use client"

import * as React from "react"
import { ChevronsUpDown } from "lucide-react"

import { cn } from "@/lib/utils"

export type IOSMenuItem = {
  label: string
  icon?: React.ReactNode
  destructive?: boolean
  onSelect?: () => void
}

export interface IOSMenuButtonProps extends Omit<React.ComponentProps<"div">, "children"> {
  label?: string
  items: IOSMenuItem[]
}

export function IOSMenuButton({ label = "Options", items, className, ...props }: IOSMenuButtonProps) {
  const [open, setOpen] = React.useState(false)
  const root = React.useRef<HTMLDivElement>(null)
  const menuId = React.useId()

  React.useEffect(() => {
    if (!open) return
    const onDown = (event: PointerEvent) => {
      if (!root.current?.contains(event.target as Node)) setOpen(false)
    }
    const onKey = (event: KeyboardEvent) => {
      if (event.key === "Escape") setOpen(false)
    }
    document.addEventListener("pointerdown", onDown)
    document.addEventListener("keydown", onKey)
    return () => {
      document.removeEventListener("pointerdown", onDown)
      document.removeEventListener("keydown", onKey)
    }
  }, [open])

  return (
    <div ref={root} className={cn("relative inline-block", className)} {...props}>
      <button
        type="button"
        aria-haspopup="menu"
        aria-expanded={open}
        aria-controls={open ? menuId : undefined}
        onClick={() => setOpen((value) => !value)}
        className={cn(
          "inline-flex h-9 items-center gap-1.5 rounded-full bg-[#7878801f] pr-3 pl-4 text-[15px] font-medium text-[#0064d2] select-none dark:bg-[#7878805c] dark:text-[#6cb2ff]",
          "transition-[transform,opacity] duration-150 active:scale-95 active:opacity-70",
          "focus-visible:ring-[3px] focus-visible:ring-[#007aff]/40 focus-visible:outline-none"
        )}
      >
        {label}
        <ChevronsUpDown className="size-3.5" strokeWidth={2.6} />
      </button>
      {open && (
        <ul
          id={menuId}
          role="menu"
          className="absolute top-full left-0 z-10 mt-2 min-w-56 origin-top-left animate-in overflow-hidden rounded-[14px] bg-white/85 py-1 text-[17px] shadow-[0_12px_40px_rgb(0_0_0/0.22)] backdrop-blur-2xl duration-200 fade-in zoom-in-95 dark:bg-neutral-800/85"
        >
          {items.map((item) => (
            <li key={item.label} role="none" className="relative not-last:after:absolute not-last:after:inset-x-4 not-last:after:bottom-0 not-last:after:h-px not-last:after:bg-black/10 dark:not-last:after:bg-white/10">
              <button
                type="button"
                role="menuitem"
                onClick={() => {
                  item.onSelect?.()
                  setOpen(false)
                }}
                className={cn(
                  "flex w-full items-center justify-between gap-6 px-4 py-2.5 text-left active:bg-black/10 dark:active:bg-white/10",
                  item.destructive ? "text-[#ff3b30]" : "text-neutral-900 dark:text-white"
                )}
              >
                {item.label}
                {item.icon && <span className="[&_svg]:size-[18px]">{item.icon}</span>}
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}
```
