Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Overlays

Dropdown Menu

Overlays

Dropdown Menu React component for Tailwind CSS v4

DrawerFamily Drawer
Live props
State
defaultOpen
Content
title
description
sheetTitle
showCancel
cancelLabel
backLabel
Appearance
size
radius
minWidth192px
Layout
side
align
sideOffset8px
Behavior
loop
modal
responsive
shortcut
Code
import { DropdownMenu } from "@/components/ui/dropdown-menu"

<DropdownMenu />
  1. Home
  2. Components
  3. Overlays
  4. Dropdown Menu
Free

Dropdown Menu React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React dropdown menu: icons, descriptions, shortcuts, badges, checkbox and radio items, submenus, and an action sheet on phones.

Click the trigger and the menu grows out of it on a quick spring. Hover or use the arrow keys to move the highlight (it wraps around), type a letter to jump to an item, Right opens a submenu and Left closes it, Enter or Space picks, Escape closes and focus returns to the trigger. The shortcuts written next to items run them while the menu is open, and an optional shortcut opens the menu from anywhere. Checkbox and radio items tick in place (the check draws itself) and keep the menu open; danger items turn red. On a phone the same menu slides up as an action sheet: 48px rows, the title centered on top, a Cancel button where the thumb is, and drag down to dismiss. A submenu row slides a new page in from the right with a Back row while the sheet resizes to fit. Opened from the keyboard, the sheet focuses its first row; the arrow keys move between rows, Right opens a submenu page and Left goes back. With reduced motion the menu and the pages fade instead of moving.

Dropdown Menu is a free React component in the React overlay components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add dropdown-menu, together with Button, Drawer, Motion presets, useControllableState, useMediaQuery and 2 more and the npm packages @radix-ui/react-dropdown-menu, lucide-react and motion.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add dropdown-menu

Usage

Import DropdownMenu from @/components/ui/dropdown-menu.

import { Copy, Ellipsis, Pencil, Share2, Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { DropdownMenu, type MenuEntry } from "@/components/ui/dropdown-menu"

export function ListingActions({ onEdit, onDelete }: { onEdit: () => void; onDelete: () => void }) {
  const items: MenuEntry[] = [
    { label: "Edit", icon: <Pencil />, shortcut: "mod+e", onSelect: onEdit },
    { label: "Duplicate", icon: <Copy />, shortcut: "mod+d" },
    {
      type: "submenu",
      label: "Share",
      icon: <Share2 />,
      items: [{ label: "WhatsApp" }, { label: "Copy link", shortcut: "mod+c" }],
    },
    { type: "separator" },
    // a confirm dialog waits a frame, so focus is back on the trigger when it opens
    { label: "Delete", icon: <Trash2 />, tone: "danger", onSelect: () => requestAnimationFrame(onDelete) },
  ]

  return (
    <DropdownMenu
      align="end"
      items={items}
      title="Pegas city bike"
      description="$420 · Brooklyn"
      trigger={
        <Button variant="ghost" size="icon" radius="full" aria-label="Listing actions">
          <Ellipsis />
        </Button>
      }
    />
  )
}

Props

  • defaultOpendefault false
    boolean

    Starts open (uncontrolled). Pass open and onOpenChange instead to control it.

  • open
    boolean

    Controlled open state. The root keeps it across the 768px breakpoint, so resizing never closes the menu.

  • onOpenChange
    (open: boolean) => void

    Called when the menu opens or closes: the trigger, an item, Escape, an outside click, the sheet's Cancel or a drag.

  • items
    MenuEntry[]

    The entries: items ({ label, icon, description, shortcut, badge, onSelect, href, tone: "danger", keepOpen }), { type: "checkbox", checked, onCheckedChange }, { type: "radio", value, onValueChange, options }, { type: "submenu", items }, { type: "group", label, items }, { type: "label" } and { type: "separator" }. Groups and radio sets get separators around them on their own. An onSelect that opens a dialog should wait a frame (requestAnimationFrame) so focus returns to the trigger first. With no enabled entry the trigger is disabled.

  • trigger
    ReactNode

    The element that opens it, rendered with asChild: a single element that takes a ref (a Button, an Avatar wrapped in a button). It gets aria-keyshortcuts when a shortcut is set.

  • children
    ReactNode

    Extra rows after the items, built from the parts (DropdownMenuItem and friends). They render in the sheet too.

  • titledefault ""
    ReactNode

    The action sheet's header on phones, centered and small like iOS (usually what the actions apply to). Not shown on desktop. The demo shows its sample while this is empty.

  • descriptiondefault ""
    ReactNode

    A quieter line under the sheet title, linked with aria-describedby. The demo shows its sample while this is empty.

  • sheetTitledefault "Actions"
    string

    The sheet's title for screen readers when there is no visible title.

  • showCanceldefault true
    boolean

    The sheet's Cancel button at the bottom, where the thumb is. The sheet still closes on a drag, a tap outside or Escape without it.

  • cancelLabeldefault "Cancel"
    string

    The label of the sheet's Cancel button.

  • backLabeldefault "Back"
    string

    The back row at the top of a submenu page in the sheet.

  • sizedefault "md"
    "sm" | "md"

    Item height 32 / 36px with 13 / 14px text and 14 / 16px icons. 44px on touch either way; sheet rows are always 48px.

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    Panel corners 0 / 12 / 16 / 24px (full falls back to lg). Items keep the panel radius minus its 6px padding, so lg rows are pills.

  • minWidthdefault 192
    number

    The panel's minimum width in px. It grows for longer labels up to 384px (and never past the screen), then labels truncate.

  • sidedefault "bottom"
    "top" | "right" | "bottom" | "left"

    The side of the trigger it opens on. It flips when there is no room.

  • aligndefault "start"
    "start" | "center" | "end"

    How it lines up with the trigger. end suits a ⋯ button at the right edge of a row.

  • sideOffsetdefault 8
    number

    The gap between the trigger and the panel, in px.

  • loopdefault true
    boolean

    Arrow keys wrap from the last item to the first (desktop and sheet). The demo says which it does.

  • modaldefault true
    boolean

    While open, the page behind ignores the pointer and does not scroll (Radix). The sheet on phones is always modal.

  • responsivedefault true
    boolean

    Below 768px the menu becomes an action sheet with large rows and submenu pages. Turn it off to keep the floating menu.

  • shortcutdefault ""
    string

    A key combo in mod+k notation (⌘ on Apple, Ctrl elsewhere) that opens and closes the menu from anywhere on the page. Empty binds nothing.

  • onOpenAutoFocus
    (event: Event) => void

    Sheet only: runs when the action sheet takes focus. Opened with the keyboard it focuses the first row, opened by a tap the sheet itself; preventDefault to focus something else. The desktop menu focuses its first item on a key open (Radix).

  • container
    HTMLElement | null

    Portal target for the menu and the sheet (default document.body).

  • classNames
    Partial<Record<"content" | "item" | "icon" | "label" | "description" | "shortcut" | "separator" | "sheet", string>>

    Extra classes for the parts: the panel (and sub panels), every row, row icons, labels, descriptions, shortcuts, separators, and the sheet.

  • className
    string

    Extra classes for the panel (or the sheet), merged last. Other Radix content props (onCloseAutoFocus, alignOffset, avoidCollisions…) pass through.

Parts

  • DropdownMenuRoot

    component

    Compound API: holds the state and decides the mode once (the floating Radix menu, or the action sheet below 768px). Every part below reads it and renders for that mode, so one tree works on desktop and on phones. Props: { open, defaultOpen, onOpenChange, modal, responsive, shortcut, container, dir, children }.

  • DropdownMenuTrigger

    component

    Opens the menu; with asChild any element that takes a ref. It carries aria-keyshortcuts when the root has a shortcut. Props: Radix DropdownMenu.Trigger props (asChild).

  • DropdownMenuContent

    component

    The panel (dropdown-menu-content with data-size, data-radius, data-side) with the popover spring, or the sheet on phones. Renders items first, then JSX children, and runs item shortcuts while open. Props: { items, size, radius, minWidth, title, description, sheetTitle, showCancel, cancelLabel, backLabel, container, classNames } & Radix DropdownMenu.Content props.

  • DropdownMenuItem

    component

    A row. href renders an <a>; asChild takes your router Link and keeps the row layout around its text (a Link that prevents the click still closes the menu). In the sheet it is a 48px button. Props: { icon, description, shortcut, badge, tone: "neutral" | "danger", keepOpen, href, target, rel, asChild, disabled, onSelect }.

  • DropdownMenuCheckboxItem

    component

    A row with a leading checkbox whose check draws itself; it stays open while toggling unless keepOpen is false. Props: { checked, defaultChecked, onCheckedChange, icon, description, shortcut, keepOpen = true, disabled } ("indeterminate" allowed).

  • DropdownMenuRadioGroup

    component

    Holds one choice from a set of radio items. Props: { value, defaultValue, onValueChange }.

  • DropdownMenuRadioItem

    component

    One choice in a radio group, marked with a trailing check. Pass keepOpen={false} for a sort menu that closes on pick. Props: { value, icon, description, keepOpen = true, disabled }.

  • DropdownMenuSub

    component

    A nested menu: holds the open state of the side panel on desktop, a pushed page with a Back row in the sheet. Props: { open, defaultOpen, onOpenChange }.

  • DropdownMenuSubTrigger

    component

    The row with a chevron that opens the nested menu. Props: { icon, description, badge, disabled }.

  • DropdownMenuSubContent

    component

    The side panel (or sheet page) of a nested menu. Props: { items, sideOffset, alignOffset, container }.

  • DropdownMenuGroup

    component

    Groups related items into a section. Takes Radix Group props.

  • DropdownMenuLabel

    component

    The small muted label of a section. Takes Radix Label props.

  • DropdownMenuSeparator

    component

    A hairline between sections. Takes Radix Separator props.

  • MenuSheet

    component

    The action sheet on its own, with its own drawer root: open it from a long press or a split button (context-menu and button-group use it on phones). Props: { open, defaultOpen, onOpenChange, items, title, description, sheetTitle, showCancel, cancelLabel, backLabel, loop, container, classNames, className, children, onCloseAutoFocus }.

  • MenuItemContent

    component

    The inside of a row (checkbox, icon, label and description, badge, shortcut, chevron or radio check) for any Radix menu item; slot sets the data-slot prefix, e.g. context-menu. Props: { label, icon, description, badge, shortcut, submenu, indicator: "checkbox" | "radio", checked, size, sheet, tone, slot, classNames }.

  • MENU_CLASSES

    constant

    The shared class recipe (content, item, itemWithDescription, icon, label, description, shortcut, badge, groupLabel, separator, subChevron, check, sheetItem, sheetSeparator...), so context menus and split buttons match.

  • menuItemClass

    function

    menuItemClass({ size, radius, tone, description }): the desktop row class for a size, radius and tone. Context menus build their rows with it.

  • menuSheetItemClass

    function

    menuSheetItemClass({ tone, description }): the bottom sheet row class, 48px tall or 56px with a description.

  • MENU_ITEM_SIZES

    constant

    The row classes per size: sm is 32px and md 36px, both 44px on a coarse pointer, with matching gap, padding, type and icon size.

  • MENU_RADII

    constant

    The panel and row corners per radius; rows take the panel radius minus its 6px padding so the shapes stay concentric.

  • MenuEntry

    type

    One entry of items: an item, checkbox, radio group, submenu, group, label or separator, told apart by type.

  • DropdownMenuProps

    type

    The props of DropdownMenu, for wrappers that build menus from your own data.

Examples

User menu

An Avatar trigger, a count badge, a theme radio group and shortcuts, with the name as the sheet title on phones.

Sort menu

A radio group with keepOpen off: picking an option closes the menu and renames the button.

Column visibility

Checkbox items stay open while you toggle; the table follows along.

Nested submenus

Two levels deep. Right and Left move between levels; on a phone each level is a page.

Sizes and radii

size sm or md, and every radius; the items follow the panel's corners.

Parts with links

The parts API with asChild anchors, as with a router Link: navigation runs and the menu closes.

Action sheet on a phone

MenuSheet in a phone frame: 48px rows, a Cancel button at the thumb and submenu pages.

Dependencies

@radix-ui/react-dropdown-menulucide-reactmotionbuttondrawermotionuse-controllable-stateuse-media-queryuse-shortcutui-config

Files

  • components/ui/dropdown-menu.tsx

FAQ

Is the React Dropdown Menu component free?

Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.

How do I install Dropdown Menu?

Run npx wingo-ui@latest add dropdown-menu. It also adds the Wingo UI items it builds on (Button, Drawer, Motion presets, useControllableState, useMediaQuery and 2 more) and the npm packages @radix-ui/react-dropdown-menu, lucide-react and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/dropdown-menu.json.

How does Dropdown Menu behave on phones?

Below 768px the menu becomes an action sheet with large rows and submenu pages. Turn it off to keep the floating menu. The responsive prop controls this and is on by default.

See also

DrawerButtonuseShortcuthookAvatar

Used in

QR CodeAvatar UploadPowerPoint ViewerVideo PlayerKanbanFile managerWorkflow CanvasUser MenuAudio PlayerExcel ViewerDOCX ViewerCSV Viewer

and 45 more.

More React overlay components

Alert DialogContext MenuDialogFamily DrawerHover CardPopover
All 13 React overlay components

Articles using this component

  • React Action Sheet: iOS-Style, Async and Responsive

    Mobile UI in React · Oct 16, 2026 · 9 min read

PreviousDrawerNext Family Drawer

Pegas city bike

$420

Brooklyn · 2 days ago

ActivePhone visible24817

Click ⋯ or focus it and press Enter. It opens on the bottom side (flipping when there is no room), aligned to the start, at least 192px wide. Arrow keys wrap around; Ctrl+E, Ctrl+D and Ctrl+Backspace run their item while it is open.

Below 768px it is an action sheet titled “Pegas city bike” ($420 · Brooklyn) with “Cancel” at the bottom; Share opens a page with “‹ Back”. Untitled sheets are read as “Actions”.