Menu
Opens a list of actions from a trigger.
menu-basic-demo.tsx
"use client";import { Button } from "@/components/vip-ui/button";import { Menu, MenuItem, MenuTrigger } from "@/components/vip-ui/menu";export function MenuBasicDemo() { return ( <MenuTrigger> <Button variant="outline">Open menu</Button> <Menu aria-label="Actions"> <MenuItem>Edit</MenuItem> <MenuItem>Duplicate</MenuItem> <MenuItem>Delete</MenuItem> </Menu> </MenuTrigger> );}Show more
Installation
CLI
Manual
The CLI adds the files, packages, theme variables, and Tailwind plugin that this component uses. Before you run it, set up your project.
npm
yarn
pnpm
bun
pnpm dlx shadcn@latest add https://vip-ui.dev/r/vip-menu.jsonComposition
MenuTriggerButtonMenuPopoverPopoverArrowOptionalMenuContentOwns focus and selection.
MenuSectionOptionalRepeatsMenuItemRepeats
Icons and shortcuts
Kbd only shows the shortcut. Add the key handler yourself.
menu-demo.tsx
"use client";import { ArchiveIcon, CopyIcon, DotsThreeIcon, EyeIcon, LinkIcon, PencilSimpleIcon, TrashIcon,} from "@phosphor-icons/react";import { Button } from "@/components/vip-ui/button";import { Kbd } from "@/components/vip-ui/kbd-code";import { MenuContent, MenuItem, MenuPopover, MenuSeparator, MenuTrigger,} from "@/components/vip-ui/menu";const iconClass = "text-muted-foreground";export function MenuDemo() { return ( <MenuTrigger> <Button variant="outline"> <DotsThreeIcon size={16} aria-hidden="true" /> Project actions </Button> <MenuPopover> <MenuContent aria-label="Project actions"> <MenuItem href="/blocks/dashboard-01"> <EyeIcon size={16} aria-hidden="true" className={iconClass} /> View project </MenuItem> <MenuItem> <PencilSimpleIcon size={16} aria-hidden="true" className={iconClass} /> Rename </MenuItem> <MenuItem> <CopyIcon size={16} aria-hidden="true" className={iconClass} /> Duplicate <Kbd>Ctrl D</Kbd> </MenuItem> <MenuItem> <LinkIcon size={16} aria-hidden="true" className={iconClass} /> Copy link <Kbd>Ctrl L</Kbd> </MenuItem> <MenuSeparator /> <MenuItem isDisabled> <ArchiveIcon size={16} aria-hidden="true" className={iconClass} /> Archive </MenuItem> <MenuItem variant="destructive"> <TrashIcon size={16} aria-hidden="true" /> Delete <Kbd>Del</Kbd> </MenuItem> </MenuContent> </MenuPopover> </MenuTrigger> );}Show more
Nested menu
On small screens, the submenu opens below the item.
menu-nested-demo.tsx
"use client";import { Button } from "@/components/vip-ui/button";import { MenuContent, MenuItem, MenuPopover, MenuTrigger, SubmenuTrigger,} from "@/components/vip-ui/menu";export function MenuNestedDemo() { return ( <MenuTrigger> <Button variant="outline">File actions</Button> <MenuPopover> <MenuContent aria-label="File actions"> <MenuItem>Rename</MenuItem> <MenuItem>Duplicate</MenuItem> <SubmenuTrigger> <MenuItem>Move to</MenuItem> <MenuPopover> <MenuContent> <MenuItem>Design</MenuItem> <MenuItem>Engineering</MenuItem> <MenuItem>Marketing</MenuItem> </MenuContent> </MenuPopover> </SubmenuTrigger> </MenuContent> </MenuPopover> </MenuTrigger> );}Show more
Selection
menu-selection-demo.tsx
"use client";import { ChatCircleIcon, GridFourIcon, KanbanIcon, ListBulletsIcon, RulerIcon, SquaresFourIcon,} from "@phosphor-icons/react";import { useState } from "react";import type { Selection } from "react-aria-components";import { Button } from "@/components/vip-ui/button";import { MenuContent, MenuItem, MenuPopover, MenuTrigger,} from "@/components/vip-ui/menu";const views = [ { id: "list", label: "List", icon: ListBulletsIcon }, { id: "grid", label: "Grid", icon: SquaresFourIcon }, { id: "board", label: "Board", icon: KanbanIcon },];const layers = [ { id: "guides", label: "Guides", icon: GridFourIcon }, { id: "rulers", label: "Rulers", icon: RulerIcon }, { id: "comments", label: "Comments", icon: ChatCircleIcon, disabled: true },];export function MenuSelectionDemo() { const [view, setView] = useState<Selection>(new Set(["list"])); const [visible, setVisible] = useState<Selection>(new Set(["guides"])); const viewLabel = view === "all" ? "All" : (views.find((item) => view.has(item.id))?.label ?? "None"); return ( <div className="flex flex-wrap justify-center gap-2"> <MenuTrigger> <Button variant="outline">View: {viewLabel}</Button> <MenuPopover> <MenuContent selectionMode="single" selectedKeys={view} onSelectionChange={setView} > {views.map((item) => { const Icon = item.icon; return ( <MenuItem key={item.id} id={item.id}> <Icon size={16} aria-hidden="true" /> {item.label} view </MenuItem> ); })} </MenuContent> </MenuPopover> </MenuTrigger> <MenuTrigger> <Button variant="outline">Layers</Button> <MenuPopover> <MenuContent selectionMode="multiple" selectedKeys={visible} onSelectionChange={setVisible} > {layers.map((item) => { const Icon = item.icon; return ( <MenuItem key={item.id} id={item.id} isDisabled={item.disabled}> <Icon size={16} aria-hidden="true" /> {item.disabled ? "Comments (unavailable)" : item.label} </MenuItem> ); })} </MenuContent> </MenuPopover> </MenuTrigger> </div> );}Show more
Sections
Each MenuSection can have its own selectionMode.
menu-sections-demo.tsx
"use client";import { SlidersHorizontalIcon } from "@phosphor-icons/react";import { useState } from "react";import type { Selection } from "react-aria-components";import { Button } from "@/components/vip-ui/button";import { MenuContent, MenuItem, MenuPopover, MenuSection, MenuSeparator, MenuTrigger,} from "@/components/vip-ui/menu";export function MenuSectionsDemo() { const [sort, setSort] = useState<Selection>(new Set(["modified"])); const [shown, setShown] = useState<Selection>(new Set(["extensions"])); return ( <MenuTrigger> <Button variant="outline"> <SlidersHorizontalIcon aria-hidden="true" /> View options </Button> <MenuPopover> <MenuContent aria-label="View options"> <MenuSection title="Sort by" selectionMode="single" disallowEmptySelection selectedKeys={sort} onSelectionChange={setSort} > <MenuItem id="name">Name</MenuItem> <MenuItem id="modified">Last modified</MenuItem> <MenuItem id="size">Size</MenuItem> </MenuSection> <MenuSeparator /> <MenuSection title="Show" selectionMode="multiple" selectedKeys={shown} onSelectionChange={setShown} > <MenuItem id="extensions">File extensions</MenuItem> <MenuItem id="hidden">Hidden files</MenuItem> </MenuSection> </MenuContent> </MenuPopover> </MenuTrigger> );}Show more
Placement and arrow
Use MenuPopover and MenuContent instead of Menu to set placement.
Quarterly report.pdf
2.4 MB
menu-placement-demo.tsx
"use client";import { DotsThreeIcon, DownloadSimpleIcon, FileTextIcon, LinkIcon, PencilSimpleIcon, TrashIcon,} from "@phosphor-icons/react";import { Button } from "@/components/vip-ui/button";import { MenuContent, MenuItem, MenuPopover, MenuTrigger,} from "@/components/vip-ui/menu";import { PopoverArrow } from "@/components/vip-ui/popover";const iconClass = "text-muted-foreground";export function MenuPlacementDemo() { return ( <div className="flex w-full max-w-sm items-center gap-3 rounded-xl border border-border px-3 py-2"> <FileTextIcon size={20} aria-hidden="true" className={iconClass} /> <div className="min-w-0 flex-1"> <p className="truncate text-sm font-medium">Quarterly report.pdf</p> <p className="text-xs text-muted-foreground">2.4 MB</p> </div> <MenuTrigger> <Button iconOnly variant="ghost" aria-label="More actions"> <DotsThreeIcon size={16} aria-hidden="true" /> </Button> <MenuPopover placement="bottom end"> <PopoverArrow /> <MenuContent aria-label="File actions"> <MenuItem> <PencilSimpleIcon size={16} aria-hidden="true" className={iconClass} /> Rename </MenuItem> <MenuItem> <DownloadSimpleIcon size={16} aria-hidden="true" className={iconClass} /> Download </MenuItem> <MenuItem> <LinkIcon size={16} aria-hidden="true" className={iconClass} /> Copy link </MenuItem> <MenuItem> <TrashIcon size={16} aria-hidden="true" className={iconClass} /> Delete </MenuItem> </MenuContent> </MenuPopover> </MenuTrigger> </div> );}Show more
API reference
vip/ui adds or changes the props below. For all other props, see React Aria Menu (opens in a new tab).
MenuItem
- variant: "default" | "destructive" = "default"
- destructive colors the text and icon for an action that deletes or removes data.
MenuSection
- title: string
- The section heading.