shadcn.io is not affiliated with official shadcn/ui
Shadcn Menu for React and Tailwind
List of options on button interaction.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/menu.jsonpnpm dlx shadcn@latest add https://kit.dev/r/menu.jsonnpx shadcn@latest add https://kit.dev/r/menu.jsonyarn shadcn@latest add https://kit.dev/r/menu.jsonInstall the following dependencies:
bun add @ark-ui/react tailwind-variants lucide-reactpnpm add @ark-ui/react tailwind-variants lucide-reactnpm install @ark-ui/react tailwind-variants lucide-reactyarn add @ark-ui/react tailwind-variants lucide-reactCopy and paste the following code into your project.
"use client";
import { Portal } from "@ark-ui/react/portal";
import { ark } from "@ark-ui/react/factory";
import {
Menu as ArkMenu,
type MenuContentProps,
useMenuContext,
} from "@ark-ui/react/menu";
import { CheckIcon, ChevronRight } from "lucide-react";
import type React from "react";
import { tv, type VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils";
export const useMenu = useMenuContext;
export const Menu = (props: React.ComponentProps<typeof ArkMenu.Root>) => {
const {
lazyMount = true,
positioning = { placement: "bottom-end" },
unmountOnExit = true,
...rest
} = props;
return (
<ArkMenu.Root
data-slot="menu"
lazyMount={lazyMount}
positioning={positioning}
unmountOnExit={unmountOnExit}
{...rest}
/>
);
};
export const MenuTrigger = (
props: React.ComponentProps<typeof ArkMenu.Trigger>
) => <ArkMenu.Trigger data-slot="menu-trigger" {...props} />;
export const MenuPositioner = (
props: React.ComponentProps<typeof ArkMenu.Positioner>
) => {
const { className, ...rest } = props;
return (
<ArkMenu.Positioner
className={cn("outline-none", className)}
data-slot="menu-positioner"
{...rest}
/>
);
};
export const menuContentVariants = tv({
base: [
"z-[calc(50+var(--nested-layer-count,0))]",
"max-h-(--available-height) not-[class*='w-']:min-w-32",
"p-1",
"bg-popover",
"text-popover-foreground",
"rounded-xl border shadow-lg/5",
"origin-(--transform-origin)",
"outline-none",
"overflow-y-auto",
"duration-100",
"data-[state=open]:animate-in",
"data-[state=open]:fade-in-0",
"data-[state=open]:zoom-in-[98%]",
"data-[placement=bottom]:slide-in-from-top-2",
"data-[placement=left]:slide-in-from-end-2",
"data-[placement=right]:slide-in-from-start-2",
"data-[placement=top]:slide-in-from-bottom-2",
"motion-reduce:animate-none!",
],
});
export const MenuContent = (props: MenuContentProps) => {
const { className, children, ...rest } = props;
return (
<Portal>
<MenuPositioner>
<ArkMenu.Content
className={cn(menuContentVariants(), className)}
data-slot="menu-content"
{...rest}
>
{children}
</ArkMenu.Content>
</MenuPositioner>
</Portal>
);
};
interface MenuGroupProps
extends React.ComponentProps<typeof ArkMenu.ItemGroup> {
/**
* The heading of the menu item group.
*/
heading?: string;
}
export const MenuGroup = (props: MenuGroupProps) => {
const { heading, children, ...rest } = props;
return (
<ArkMenu.ItemGroup data-slot="menu-group" {...rest}>
{!!heading && <MenuGroupLabel>{heading}</MenuGroupLabel>}
{children}
</ArkMenu.ItemGroup>
);
};
export const MenuSeparator = (
props: React.ComponentProps<typeof ArkMenu.Separator>
) => {
const { className, ...rest } = props;
return (
<ArkMenu.Separator
className={cn("my-1 h-px bg-border", className)}
data-slot="menu-separator"
{...rest}
/>
);
};
const menuItemVariants = tv({
base: [
"group/menu-item",
"relative",
"w-full",
"px-2.5 py-1.5",
"flex items-center gap-2",
"select-none text-sm",
"rounded-lg",
"outline-hidden",
"group-data-[date=open]/trigger-item:bg-accent group-data-[date=open]/trigger-item:text-accent-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-64",
"[&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
],
variants: {
variant: {
default: [
"data-highlighted:bg-accent data-highlighted:text-accent-foreground",
],
destructive: [
"text-destructive dark:text-destructive-foreground",
"data-highlighted:bg-destructive/10 dark:data-highlighted:bg-destructive-foreground/10",
"**:[svg]:text-destructive! dark:**:[svg]:text-destructive-foreground!",
],
},
},
defaultVariants: {
variant: "default",
},
});
interface MenuItemProps
extends React.ComponentProps<typeof ArkMenu.Item>,
VariantProps<typeof menuItemVariants> {}
export const MenuItem = (props: MenuItemProps) => {
const { variant = "default", className, ...rest } = props;
return (
<ArkMenu.Item
className={cn(menuItemVariants({ variant }), className)}
data-variant={variant}
{...rest}
/>
);
};
export const MenuQuickItem = (props: MenuItemProps) => {
const { variant = "default", className, ...rest } = props;
return (
<ArkMenu.Item
className={cn(
menuItemVariants({ variant }),
"flex-col gap-1",
"[&_svg:not([class*='size-'])]:size-4.5",
className
)}
{...rest}
/>
);
};
export const MenuCheckboxItem = (
props: React.ComponentProps<typeof ArkMenu.CheckboxItem>
) => {
const { className, children, ...rest } = props;
return (
<ArkMenu.CheckboxItem
className={cn(
menuItemVariants({ variant: "default" }),
"ps-8",
className
)}
{...rest}
>
<ArkMenu.ItemIndicator
className={cn(
"absolute inset-s-2",
"size-3.5",
"flex items-center justify-center",
"pointer-events-none"
)}
>
<CheckIcon />
</ArkMenu.ItemIndicator>
<ArkMenu.ItemText>{children}</ArkMenu.ItemText>
</ArkMenu.CheckboxItem>
);
};
interface MenuRadioGroupProps
extends React.ComponentProps<typeof ArkMenu.RadioItemGroup> {
/**
* The heading of the menu radio item group.
*/
heading?: string;
}
export const MenuRadioGroup = (props: MenuRadioGroupProps) => {
const { heading, children, ...rest } = props;
return (
<ArkMenu.RadioItemGroup data-slot="menu-radio-group" {...rest}>
{!!heading && <MenuGroupLabel>{heading}</MenuGroupLabel>}
{children}
</ArkMenu.RadioItemGroup>
);
};
export const MenuGroupLabel = (
props: React.ComponentProps<typeof ArkMenu.ItemGroupLabel>
) => {
const { className, ...rest } = props;
return (
<ArkMenu.ItemGroupLabel
className={cn(
"px-2 py-1.5",
"font-medium text-muted-foreground text-sm",
"pointer-events-none",
className
)}
data-slot="menu-group-label"
{...rest}
/>
);
};
export const MenuRadioItem = (
props: React.ComponentProps<typeof ArkMenu.RadioItem>
) => {
const { className, children, ...rest } = props;
return (
<ArkMenu.RadioItem
className={cn(
menuItemVariants({ variant: "default" }),
"ps-8",
className
)}
data-slot="menu-radio-item"
{...rest}
>
<ArkMenu.ItemIndicator className="pointer-events-none absolute inset-s-2 flex size-3.5 items-center justify-center">
<CheckIcon />
</ArkMenu.ItemIndicator>
<ArkMenu.ItemText data-slot="menu-radio-item-text">
{children}
</ArkMenu.ItemText>
</ArkMenu.RadioItem>
);
};
export const MenuSub = (props: React.ComponentProps<typeof Menu>) => (
<Menu data-slot="menu-sub" {...props} />
);
export const MenuSubContent = (
props: React.ComponentProps<typeof ArkMenu.Content>
) => {
const { className, ...rest } = props;
return (
<Portal>
<MenuPositioner data-slot="menu-sub-positioner">
<ArkMenu.Content
className={cn(menuContentVariants(), className)}
data-slot="menu-sub-content"
{...rest}
/>
</MenuPositioner>
</Portal>
);
};
export const MenuSubTrigger = (
props: React.ComponentProps<typeof ArkMenu.TriggerItem>
) => {
const { className, children, ...rest } = props;
return (
<ArkMenu.TriggerItem
className={cn(menuItemVariants({ variant: "default" }), className)}
data-slot="menu-sub-trigger"
{...rest}
>
{children}
<MenuShortcut>
<ChevronRight />
</MenuShortcut>
</ArkMenu.TriggerItem>
);
};
export const MenuShortcut = (props: React.ComponentProps<typeof ark.span>) => {
const { className, ...rest } = props;
return (
<ark.span
className={cn(
"ms-auto rtl:me-auto",
"text-muted-foreground text-xs tracking-widest",
"group-data-highlighted/menu-item:group-data-[variant=destructive]/menu-item:text-destructive dark:group-data-highlighted/menu-item:group-data-[variant=destructive]/menu-item:text-destructive-foreground",
className
)}
data-slot="menu-shortcut"
{...rest}
/>
);
};
export const MenuArrow = (
props: React.ComponentProps<typeof ArkMenu.Arrow>
) => {
const { style, ...rest } = props;
return (
<ArkMenu.Arrow
style={
{
"--arrow-background": "var(--popover)",
"--arrow-size": "calc(1.5 * var(--spacing))",
...style,
left: "20px",
} as React.CSSProperties
}
{...rest}
>
<ArkMenu.ArrowTip className="border-s border-t" />
</ArkMenu.Arrow>
);
};Update the import paths to match your project setup.
Anatomy
MenuArrow
└── MenuArrowTip
MenuSubContent
MenuContextTrigger
MenuIndicator
MenuQuickItem
└── MenuGroup
└── MenuGroupLabel
MenuItemIndicator
MenuItemText
MenuPositioner
MenuSeparator
MenuTrigger
└── MenuSubTriggerUsage
import {
Archive,
ArchiveX,
Bell,
CirclePlus,
FolderInput,
MailX,
Reply,
Send,
SquarePen,
Trash,
Trash2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Menu,
MenuCheckboxItem,
MenuContent,
MenuGroup,
MenuItem,
MenuRadioGroup,
MenuRadioItem,
MenuSeparator,
MenuShortcut,
MenuSub,
MenuSubContent,
MenuSubTrigger,
MenuTrigger,
} from "@/components/ui/menu";<Menu>
<MenuTrigger asChild>
<Button variant="outline">Open</Button>
</MenuTrigger>
<MenuContent className="w-40">
<MenuGroup>
<MenuItem value="forward">
<Send /> Forward
<MenuShortcut>⌘F</MenuShortcut>
</MenuItem>
<MenuItem value="reply">
<Reply /> Reply
<MenuShortcut>⌘R</MenuShortcut>
</MenuItem>
<MenuItem value="archive">
<Archive /> Archive
<MenuShortcut>⌘Z</MenuShortcut>
</MenuItem>
<MenuSub>
<MenuSubTrigger>
<FolderInput /> Move to
</MenuSubTrigger>
<MenuSubContent>
<MenuItem value="move-to-folder-1">
<ArchiveX /> Junk
</MenuItem>
<MenuItem value="move-to-folder-2">
<Trash /> Trash
</MenuItem>
<MenuItem value="move-to-folder-3">
<Bell /> Reminders
</MenuItem>
<MenuSub>
<MenuSubTrigger>
<CirclePlus />
More
</MenuSubTrigger>
<MenuSubContent>
<MenuItem value="move-to-folder-4">
<SquarePen />
Drafts
</MenuItem>
<MenuItem value="move-to-folder-6">
<MailX />
Spam
</MenuItem>
</MenuSubContent>
</MenuSub>
</MenuSubContent>
</MenuSub>
<MenuSeparator />
<MenuRadioGroup heading="Priority" value="medium">
<MenuRadioItem value="low">Low</MenuRadioItem>
<MenuRadioItem value="medium">Medium</MenuRadioItem>
<MenuRadioItem value="high">High</MenuRadioItem>
</MenuRadioGroup>
<MenuSeparator />
<MenuCheckboxItem checked value="block">
Block sender
</MenuCheckboxItem>
<MenuSeparator />
<MenuItem value="delete" variant="destructive">
<Trash2 /> Delete
<MenuShortcut>⌘ ⌫</MenuShortcut>
</MenuItem>
</MenuGroup>
</MenuContent>
</Menu>Accessibility
| Key | Description |
|---|---|
Enter | Activate the focused control. |
Space | Activate the focused control. |
ArrowDown | Move down. |
ArrowUp | Move up. |
ArrowLeft | Move left. |
ArrowRight | Move right. |
Home | Go to the start. |
End | Go to the end. |
Demos
Checkboxes
Destructive
Dialog
Group Label
Icons
Link
Nested
Positioning
Quick Item
Radio Group
Shortcuts
With Scroll
With Separator
API Reference
Menu
PropType
AttributeType
MenuArrow
div
PropType
AttributeType
CSS variableType
MenuArrowTip
div
PropType
AttributeType
CSS variableType
MenuSubContent
div
PropType
AttributeType
MenuQuickItem
div
PropType
AttributeType
MenuGroup
div
PropType
AttributeType
MenuGroupLabel
div
PropType
AttributeType
MenuItemIndicator
div
PropType
AttributeType
MenuItemText
div
PropType
AttributeType
MenuPositioner
div
PropType
AttributeType
MenuSeparator
hr
PropType
AttributeType
MenuTrigger
button
PropType
AttributeType
MenuSubTrigger
div
PropType
AttributeType
MenuContent
div
PropType
AttributeType
MenuItem
div
PropType
AttributeType
MenuCheckboxItem
div
PropType
AttributeType
MenuRadioGroup
div
PropType
AttributeType
MenuRadioItem
div
PropType
AttributeType
MenuSub
PropType
AttributeType
MenuShortcut
span
PropType
AttributeType
useMenu
PropType