A menu of actions or options that opens from a button.
Dropdown Menu is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.
Dropdown menu wraps the menu primitive of the chosen base. The DropdownMenu root manages the open state and DropdownMenuTrigger opens it. DropdownMenuContent renders the menu in a portal, positioned below the trigger. Inside it, use DropdownMenuItem for actions, DropdownMenuCheckboxItem for toggles, and DropdownMenuRadioGroup with DropdownMenuRadioItem for a single choice. Organize items with DropdownMenuGroup, DropdownMenuLabel and DropdownMenuSeparator, and nest a submenu with DropdownMenuSub, DropdownMenuSubTrigger and DropdownMenuSubContent. DropdownMenuShortcut displays a keyboard shortcut at the end of an item.
Use a dropdown menu for actions and settings behind a visible button. For actions on an object opened with a right-click, use a context menu; to choose a value for a form field, use a select.
The menu is uncontrolled by default; set defaultOpen to start open, or control it with open and onOpenChange. Render the trigger as another component, such as a Button, with the render prop.
The menu is uncontrolled by default; set defaultOpen to start open, or control it with open and onOpenChange. Set asChild on DropdownMenuTrigger to render your own Button as the trigger.
The menu is uncontrolled by default; set defaultOpen to start open, or control it with isOpen and onOpenChange. The Button placed first inside DropdownMenu opens it.
Selecting a regular item closes the menu.
Checkbox and radio items keep the menu open so several choices can be made in a row.
Checkbox items are uncontrolled with defaultChecked, or controlled with checked and onCheckedChange. Radio groups take defaultValue, or value and onValueChange, and each radio item needs a value.
Checkbox items are uncontrolled with defaultSelected, or controlled with isSelected and onChange. Radio groups take defaultValue, or value and onChange, and each radio item needs a value.
Set inset on items and labels without an icon to align their text with items that have one, and set variant="destructive" on items that delete or remove something.
Examples
Checkboxes
Controlled checkbox items that toggle interface elements, including a disabled item.
Radio group
A controlled radio group for choosing a single panel position.
Inset
A labelled group of inset items without icons, each with a keyboard shortcut.
Also accepts the props of the primitive part it wraps.
02
DropdownMenuPortal
Renders its children into a portal outside the current DOM hierarchy.
Also accepts the props of the primitive part it wraps.
03
DropdownMenuTrigger
The button that opens the menu.
Also accepts the props of the primitive part it wraps.
04
DropdownMenuContent
The menu panel, rendered in a portal and positioned relative to the trigger.
Also accepts the props of the primitive parts it wraps.
05
DropdownMenuGroup
Groups related items.
Also accepts the props of the primitive part it wraps.
06
DropdownMenuLabel
A heading for a group of items.
Prop
Type
Default
Description
inset
boolean
-
Adds leading padding so the label aligns with items that have an icon or indicator.
Also accepts the props of the primitive part it wraps.
07
DropdownMenuItem
An action in the menu.
Prop
Type
Default
Description
inset
boolean
-
Adds leading padding so the item aligns with items that have an icon or indicator.
variant
"default" | "destructive"
"default"
destructive colors the item and its icon with the destructive color.
Also accepts the props of the primitive part it wraps.
08
DropdownMenuSub
The root of a nested submenu.
Also accepts the props of the primitive part it wraps.
09
DropdownMenuSubTrigger
The item that opens a submenu, shown with a trailing caret.
Prop
Type
Default
Description
inset
boolean
-
Adds leading padding so the trigger aligns with items that have an icon or indicator.
Also accepts the props of the primitive part it wraps.
10
DropdownMenuSubContent
The panel of a submenu, opened beside its trigger.
Also accepts the props of React.ComponentProps<typeof DropdownMenuContent>.
11
DropdownMenuCheckboxItem
An item that toggles a setting on or off and shows a check mark when on.
Prop
Type
Default
Description
inset
boolean
-
Adds leading padding so the item aligns with other inset items.
Also accepts the props of the primitive part it wraps.
12
DropdownMenuRadioGroup
Groups radio items so that only one can be selected.
Also accepts the props of the primitive part it wraps.
13
DropdownMenuRadioItem
An item in a radio group that shows a check mark when selected.
Prop
Type
Default
Description
inset
boolean
-
Adds leading padding so the item aligns with other inset items.
Also accepts the props of the primitive part it wraps.
14
DropdownMenuSeparator
A divider between groups of items.
Also accepts the props of the primitive part it wraps.
15
DropdownMenuShortcut
Displays a keyboard shortcut at the end of an item.
Prop
Type
Default
Description
mod
boolean
-
Prefixes the shortcut with the platform modifier key, ⌘ on Apple platforms and Ctrl+ elsewhere.
Also accepts the props of React.ComponentProps<"span">.
Accessibility
The trigger sets aria-expanded. The menu has the menu role; items have the menuitem, menuitemcheckbox or menuitemradio role, and checkbox and radio items set aria-checked.
Groups have the group role and take their name from DropdownMenuLabel.
Opening the menu from the keyboard focuses the first item. Arrow keys move between items, and disabled items set aria-disabled.
On a submenu trigger, the Right Arrow opens the submenu and moves focus into it, and the Left Arrow returns to the trigger. The arrow directions follow the reading direction.
Escape closes the menu and returns focus to the trigger.
DropdownMenuShortcut only displays a shortcut; it does not register one. It is hidden on small screens.
Notes
Styling hooks: data-slot values such as dropdown-menu-content, dropdown-menu-item, dropdown-menu-checkbox-item, dropdown-menu-checkbox-item-indicator, dropdown-menu-radio-item, dropdown-menu-radio-item-indicator, dropdown-menu-sub-trigger, dropdown-menu-label, dropdown-menu-separator and dropdown-menu-shortcut.
Items set data-inset and data-variant; disabled items get data-disabled, and an open submenu trigger gets data-popup-open.
By default the menu opens below the trigger, aligned to start, and matches the trigger's width with a minimum of 12rem. Override the width with className.
Uses the popover, accent, destructive and border colors.
DropdownMenu opens DropdownMenuContent next to a DropdownMenuTrigger when it is pressed. The content renders through a portal (inside a full-window overlay on iOS) as a raised, rounded card positioned below the trigger by default.
Items can be plain actions, checkbox items, radio items in a group, or a submenu. DropdownMenuSubContent expands in place under its trigger, and the caret turns to point down while it is open. For a menu that opens on long press at the touch point, use Context Menu.
Add the pieces it builds on: Icon, Text, and the primitives components/primitives/menu.tsx, components/primitives/use-render.tsx, and the animated view components/ui/animated-view.tsx from the next step.
Copy the animated view into components/ui/animated-view.tsx:
Add the pieces it builds on: Icon, Text, and the primitives components/primitives/menu.tsx, components/primitives/use-render.tsx, and the animated view components/ui/animated-view.tsx from the next step.
Copy the animated view into components/ui/animated-view.tsx:
Copy the source into components/ui/dropdown-menu.tsx:
Add the pieces it builds on: Icon, Text, and the primitives components/primitives/menu.tsx, components/primitives/use-render.tsx, and the animated view components/ui/animated-view.tsx from the next step.
Copy the animated view into components/ui/animated-view.tsx:
Copy the source into components/ui/dropdown-menu.tsx:
import SwiftUIimport UIKitstruct RGBA: Sendable { let red: CGFloat let green: CGFloat let blue: CGFloat let alpha: CGFloat init(_ hex: UInt32, _ alpha: CGFloat = 1) { red = CGFloat((hex >> 16) & 0xFF) / 255 green = CGFloat((hex >> 8) & 0xFF) / 255 blue = CGFloat(hex & 0xFF) / 255 self.alpha = alpha }
swift/Theme/Tokens.swift
import SwiftUIpublic enum EsuissColor { public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000)) public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7)) public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F)) public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7)) public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F)) public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7)) public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF)) public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF)) public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1)) public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7)) public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617)) public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6)) public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
Copy the source and every file it uses:
swift/Components/DropdownMenu.swift
import PhosphorSwiftimport SwiftUIenum MenuPresentation: Sendable { case system, popup}extension EnvironmentValues { @Entry var menuPresentation: MenuPresentation = .system @Entry var menuRadio = MenuRadioContext()}struct MenuRadioContext: @unchecked Sendable { var selected: AnyHashable? var select: @MainActor @Sendable (AnyHashable) -> Void = { _ in }}
swift/Components/Button.swift
import SwiftUIpublic struct VariantButtonStyle: ButtonStyle { public enum Variant: CaseIterable, Sendable { case `default`, outline, secondary, tinted, ghost, plain, destructive, link } public enum Size: CaseIterable, Sendable { case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl var height: CGFloat { switch self { case .xs, .iconXs: 24 case .sm, .iconSm: 32 case .default, .icon: 36 case .lg, .iconLg: 44
swift/Components/ButtonGroup.swift
import SwiftUIextension EnvironmentValues { @Entry var buttonGroup: Axis? = nil @Entry var buttonGroupSize: CGSize = .zero}enum ButtonGroupSpace: Hashable { case group}struct ButtonGroupFrame { let bounds: Anchor<CGRect> let outlined: Bool}
swift/Components/Field.swift
import SwiftUIextension EnvironmentValues { @Entry public var isInvalid = false @Entry var fieldInset = false @Entry var fieldGroupInset = false @Entry var fieldSetInset = false}extension View { public func invalid(_ isInvalid: Bool = true) -> some View { environment(\.isInvalid, isInvalid) }}public enum FieldOrientation: CaseIterable, Sendable {
swift/Components/Overlay.swift
import PhosphorSwiftimport Observationimport SwiftUIpublic enum PopupAlignment: Sendable, CaseIterable { case start, center, end}struct OverlayDismiss: Sendable { let action: @MainActor @Sendable () -> Void @MainActor func callAsFunction() { action() }}
swift/Components/Select.swift
import PhosphorSwiftimport SwiftUIstruct SelectContext: @unchecked Sendable { var selected: AnyHashable? var registering = false var select: @MainActor @Sendable (AnyHashable) -> Void = { _ in }}extension EnvironmentValues { @Entry var select = SelectContext()}struct SelectLabelsKey: PreferenceKey { static var defaultValue: [AnyHashable: Text] { [:] }
package com.esuiss.ui.themeimport androidx.compose.runtime.Immutableimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.unit.dp@Immutabledata class Colors( val background: Color, val foreground: Color, val card: Color, val cardForeground: Color, val popover: Color, val popoverForeground: Color, val primary: Color, val primaryForeground: Color,
Use render on DropdownMenuTrigger to render it as a Button. Items close the menu when pressed unless closeOnPress={false}.
Use DropdownMenuCheckboxItem with checked and onCheckedChange, and DropdownMenuRadioItem inside a DropdownMenuRadioGroup with value and onValueChange. Group items with DropdownMenuGroup and DropdownMenuLabel, and divide sections with DropdownMenuSeparator.
Set variant="destructive" on an item that deletes or removes. Use inset on items without an icon to align them with items that have one. Position the content with side, align, sideOffset and alignOffset.
Blocks interaction with the rest of the screen while open.
disabled
boolean
false
Prevents the menu from opening.
02
DropdownMenuContent
Floating menu card.
Prop
Type
Default
Description
portalHost
string
-
Name of the portal host to render into.
Also accepts the props of React.ComponentProps<typeof MenuPrimitive.Popup>, PositionerProps.
03
DropdownMenuSubTrigger
Row that expands the submenu.
Prop
Type
Default
Description
children
React.ReactNode
-
iconClassName
string
-
Extra classes for the caret icon.
inset
boolean
-
Adds start padding.
Also accepts the props of React.ComponentProps<typeof MenuPrimitive.SubmenuTrigger>.
04
DropdownMenuSubContent
Items of the submenu.
Also accepts the props of React.ComponentProps<typeof MenuPrimitive.Popup>.
05
DropdownMenuItem
Action row.
Prop
Type
Default
Description
inset
boolean
-
Adds start padding to align with icon items.
variant
"default" | "destructive"
"default"
default or destructive.
Also accepts the props of React.ComponentProps<typeof MenuPrimitive.Item>.
06
DropdownMenuCheckboxItem
Row with a check mark that toggles.
Prop
Type
Default
Description
children
React.ReactNode
-
inset
boolean
-
Adds start padding.
Also accepts the props of React.ComponentProps<typeof MenuPrimitive.CheckboxItem>.
07
DropdownMenuRadioItem
Row that selects one value in its group.
Prop
Type
Default
Description
children
React.ReactNode
-
inset
boolean
-
Adds start padding.
Also accepts the props of React.ComponentProps<typeof MenuPrimitive.RadioItem>.
08
DropdownMenuLabel
Heading for a group.
Prop
Type
Default
Description
inset
boolean
-
Adds start padding.
Also accepts the props of React.ComponentProps<typeof MenuPrimitive.GroupLabel>.
09
DropdownMenuSeparator
Line between sections.
Also accepts the props of React.ComponentProps<typeof MenuPrimitive.Separator>.
Accessibility
The trigger has the button role.
The popup has the menu role; items use menuitem, checkbox and radio roles, and radio groups use radiogroup.
Groups are labelled by their DropdownMenuLabel, and separators have the separator role.
Notes
Uses bg-popover, and bg-accent for an open submenu trigger; a highlighted item fades to opacity-80 and a disabled one to opacity-50; light and dark follow the system appearance.
The fade-in respects the system Reduce Motion setting.
import { cn } from "cn"import { CaretRightIcon, CheckIcon } from "phosphor-react-native"import * as React from "react"import { Platform, View } from "react-native"import { FadeIn, ReduceMotion } from "react-native-reanimated"import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"import * as MenuPrimitive from "@/components/primitives/menu"import { resolveClassName } from "@/components/primitives/use-render"import { AnimatedView } from "@/components/ui/animated-view"import { Icon } from "@/components/ui/icon"import { TextClassContext } from "@/components/ui/text"const DropdownMenu = MenuPrimitive.Rootconst DropdownMenuPortal = MenuPrimitive.Portal
nativewind/components/ui/dropdown-menu.tsx
import { cn } from "cn"import { CaretRightIcon, CheckIcon } from "phosphor-react-native"import * as React from "react"import { Platform, View } from "react-native"import { FadeIn, ReduceMotion } from "react-native-reanimated"import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"import * as MenuPrimitive from "@/components/primitives/menu"import { resolveClassName } from "@/components/primitives/use-render"import { AnimatedView } from "@/components/ui/animated-view"import { Icon } from "@/components/ui/icon"import { TextClassContext } from "@/components/ui/text"const DropdownMenu = MenuPrimitive.Rootconst DropdownMenuPortal = MenuPrimitive.Portal
stylesheet/components/ui/dropdown-menu.tsx
import type { IconProps as PhosphorIconProps } from "phosphor-react-native"import { CaretRightIcon, CheckIcon } from "phosphor-react-native"import * as React from "react"import { Platform, type StyleProp, StyleSheet, type TextStyle, View, type ViewStyle,} from "react-native"import { FadeIn, ReduceMotion } from "react-native-reanimated"import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"import * as MenuPrimitive from "@/components/primitives/menu"import { resolveStyle } from "@/components/primitives/use-render"import { AnimatedView } from "@/components/ui/animated-view"