R

ReflexKR

Reflex Docs

The Component Workshop: Reflex's 100 Library Components

·

Course overview

Layout

·

Box

·

Flex

·

Stack

·

Grid

·

Container

·

Section

·

Center

·

Spacer

·

Separator

·

Card

·

Inset

·

Aspect Ratio

·

Fragment

Typography

·

Text

·

Heading

·

Link

·

Code

·

Markdown

·

Blockquote

·

Quote

·

Strong

·

Em

·

Kbd

Forms

·

Button

·

Input

·

Text Area

·

Checkbox

·

Switch

·

Radio Group

·

Select

·

Slider

·

Form

·

Upload

Data display

·

Badge

·

Avatar

·

Icon

·

Callout

·

List

·

Data List

·

Code Block

·

Progress

·

Spinner

·

Scroll Area

·

Moment

Disclosure

·

Tabs

·

Accordion

·

Segmented Control

Overlay

·

Tooltip

·

Popover

·

Hover Card

·

Dialog

·

Alert Dialog

·

Drawer

·

Dropdown Menu

·

Context Menu

·

Toast

Dynamic rendering

·

Cond

·

Match

·

Foreach

·

Auto Scroll

Media

·

Image

·

Audio

·

Video

Tables and data grids

·

Table

·

Data Table

·

Data Editor

Charts

·

Area Chart

·

Bar Chart

·

Line Chart

·

Composed Chart

·

Pie Chart

·

Radar Chart

·

Radial Bar Chart

·

Scatter Chart

·

Funnel Chart

·

Treemap

·

Sankey Chart

·

Error Bar

Chart parts

·

Axis

·

Cartesian Grid

·

Chart Tooltip

·

Legend

·

Label

·

Reference

·

Brush

Other chart libraries

·

Plotly

·

Pyplot

HTML elements

·

HTML

·

HTML Text

·

HTML Layout

·

HTML Forms

·

HTML Media

·

HTML Tables

·

SVG

Other tools

·

Theme

·

Skeleton

·

Clipboard

·

Html Embed

·

Script

·

Memo

The Component Workshop: Reflex's 100 Library Components
Overlay

Dropdown Menu

rx.menu.rootrx.menu.triggerrx.menu.contentrx.menu.itemrx.menu.separatorrx.menu.subrx.menu.sub_triggerrx.menu.sub_content

Official docs

Workshop order

Put a '⋯' button at the end of each row in the file list. Clicking it should open a menu for duplicating, moving or changing the status.

A dropdown menu is a list of choices that opens below a button when you click it. Use it to tuck less frequent actions behind a single button and keep the screen uncluttered. rx.menu and rx.dropdown_menu are the same thing; this page uses the shorter rx.menu.

  • menu.root: wraps all the parts of the menu.
  • menu.trigger: wraps the element that opens and closes the menu.
  • menu.content: the panel that opens.
  • menu.item: a single menu item. shortcut adds shortcut text on its right.
  • menu.separator: a line between items.
  • menu.sub: wraps all the parts of a submenu. Inside it go menu.sub_trigger (the item that opens the submenu) and menu.sub_content (the submenu panel).
rx.menu.root(
    rx.menu.trigger(
        rx.button("Options", variant="soft"),
    ),
    rx.menu.content(
        rx.menu.item("Edit", shortcut="⌘ E"),
        rx.menu.item("Duplicate", shortcut="⌘ D"),
        rx.menu.separator(),
        rx.menu.item("Archive", shortcut="⌘ N"),
        rx.menu.sub(
            rx.menu.sub_trigger("More"),
            rx.menu.sub_content(
                rx.menu.item("Move to project…"),
                rx.menu.item("Move to folder…"),
                rx.menu.separator(),
                rx.menu.item("Advanced options…"),
            ),
        ),
        rx.menu.separator(),
        rx.menu.item("Share"),
        rx.menu.item("Add to favorites"),
        rx.menu.separator(),
        rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"),
    ),
)

Try the props

The look and placement of the menu panel go on menu.content. Change a value, then click Options to see it.

Try the props

variant

size

color

high_contrast

side

align

disabled

modal

rx.menu.root(
    rx.menu.trigger(rx.button("Options", variant="soft")),
    rx.menu.content(
        rx.menu.item("Edit", shortcut="⌘ E"),
        rx.menu.item("Duplicate", shortcut="⌘ D", disabled=False),
        rx.menu.separator(),
        rx.menu.item("Delete", shortcut="⌘ ⌫", color_scheme="red"),
        variant="solid",
        size="2",
        color_scheme="indigo",
        high_contrast=False,
        side="bottom",
        align="start",
    ),
    modal=True,
)
variant="solid" fills the hovered item strongly, "soft" lightly. A disabled item cannot be chosen.
PartPropValuesUse
rootopen · default_openTrue / FalseDrive the open state from State (controlled) · open at first
rootmodalTrue / FalseTrue by default. Blocks interaction and scrolling outside while open
rooton_open_changeeventFires on open or close; passes the new open value (bool)
contentvariant · size"solid" "soft" · "1" "2"Highlight style, size
contentcolor_scheme · high_contrastcolor name · boolColor used for items, contrast
contentside · align · side_offset · align_offsetpositionSide it opens on and where it lines up
contentloopTrue / FalseKeyboard navigation wraps from the end back to the start
contentavoid_collisions · collision_padding · sticky · hide_when_detached-Screen-edge handling (same as Popover)
itemshortcutstringShortcut text shown on the right (display only)
itemcolor_scheme · disabled · text_valuecolor · bool · stringItem color, make it unselectable, text for typeahead
itemon_selecteventAn item is chosen with the mouse or keyboard
subopen · default_open · on_open_change-Open state of the submenu
sub_triggerdisabled · text_value-The item that opens the submenu
sub_contentloop · side_offset · align_offset etc.-Placement of the submenu panel

content and sub_content have the on_escape_key_down, on_pointer_down_outside, on_focus_outside and on_interact_outside events, and content also has on_close_auto_focus.

Choosing an item: on_select

When an item is clicked or chosen with the keyboard, on_select fires and the menu closes. Pass the chosen value as a handler argument and you don't need a separate handler per item. on_click, as in the official docs' dialog example, also fires for both mouse and keyboard, but on_select is the event made for choosing items.

Selected: nothing yet

class DropdownPickState(rx.State):
    last: str = "nothing yet"

    @rx.event
    def pick(self, name: str):
        self.last = name


rx.flex(
    rx.menu.root(
        rx.menu.trigger(rx.button("Sort by", rx.icon("chevron-down"), variant="outline")),
        rx.menu.content(
            rx.menu.item("Name", on_select=DropdownPickState.pick("Name")),
            rx.menu.item("Date added", on_select=DropdownPickState.pick("Date added")),
            rx.menu.item("Size", on_select=DropdownPickState.pick("Size")),
            rx.menu.separator(),
            rx.menu.item("Custom…", disabled=True),
        ),
    ),
    rx.text("Selected: ", rx.text.strong(DropdownPickState.last)),
    spacing="3",
    align="center",
    wrap="wrap",
)

Reacting to open and close: on_open_change

The on_open_change of menu.root passes True when it opens and False when it closes.

Opened or closed: 0 times

Dropdown menu open: no

class DropdownMenuEventState(rx.State):
    num_opens: int = 0
    opened: bool = False

    @rx.event
    def count_opens(self, value: bool):
        self.opened = value
        self.num_opens += 1


rx.flex(
    rx.text("Opened or closed: ", DropdownMenuEventState.num_opens, " times"),
    rx.text("Dropdown menu open: ", rx.cond(DropdownMenuEventState.opened, "yes", "no")),
    rx.menu.root(
        rx.menu.trigger(
            rx.button("Options", variant="soft", size="2"),
        ),
        rx.menu.content(
            rx.menu.item("Edit", shortcut="⌘ E"),
            rx.menu.item("Duplicate", shortcut="⌘ D"),
            rx.menu.separator(),
            rx.menu.item("Archive", shortcut="⌘ N"),
            rx.menu.separator(),
            rx.menu.item("Delete", shortcut="⌘ ⌫", color="red"),
        ),
        on_open_change=DropdownMenuEventState.count_opens,
    ),
    direction="column",
    spacing="3",
    align="start",
)

Opening a dialog from the menu

If you put a whole rx.dialog.root inside a menu item, the dialog vanishes the moment the menu closes. It lives inside the menu, so it only exists while the menu is open.

rx.menu.root(
    rx.menu.trigger(rx.icon("ellipsis-vertical")),
    rx.menu.content(
        rx.menu.item(
            rx.dialog.root(  # don't do this
                rx.dialog.trigger(rx.text("Edit")),
                rx.dialog.content(...),
            ),
        ),
    ),
)

Instead, keep the dialogs outside the menu and remember which one is open in a single State string. Items only change that value, and each dialog opens with open=State.value == "name". When it closes, on_open_change resets the value to an empty string.

class DropdownDialogState(rx.State):
    which_dialog_open: str = ""

    @rx.event
    def set_which_dialog_open(self, value: str):
        self.which_dialog_open = value

    @rx.event
    def delete(self):
        yield rx.toast("Deleted item")

    @rx.event
    def save_settings(self):
        yield rx.toast("Saved settings")


rx.vstack(
    rx.menu.root(
        rx.menu.trigger(rx.icon_button(rx.icon("menu"), variant="soft", aria_label="Menu")),
        rx.menu.content(
            rx.menu.item("Delete", on_click=DropdownDialogState.set_which_dialog_open("delete")),
            rx.menu.item("Settings", on_click=DropdownDialogState.set_which_dialog_open("settings")),
        ),
    ),
    rx.cond(
        DropdownDialogState.which_dialog_open,
        rx.text(DropdownDialogState.which_dialog_open, " dialog is open"),
    ),
    rx.alert_dialog.root(
        rx.alert_dialog.content(
            rx.alert_dialog.title("Are you Sure?"),
            rx.alert_dialog.description(
                "This action cannot be undone. Are you sure you want to delete this item?",
                margin_bottom="20px",
            ),
            rx.hstack(
                rx.alert_dialog.action(
                    rx.button("Delete", color_scheme="red", on_click=DropdownDialogState.delete),
                ),
                rx.spacer(),
                rx.alert_dialog.cancel(rx.button("Cancel")),
            ),
        ),
        open=DropdownDialogState.which_dialog_open == "delete",
        on_open_change=DropdownDialogState.set_which_dialog_open(""),
    ),
    rx.dialog.root(
        rx.dialog.content(
            rx.dialog.title("Settings"),
            rx.dialog.description("Set your settings in this settings dialog.", margin_bottom="20px"),
            rx.dialog.close(
                rx.button("Close", on_click=DropdownDialogState.save_settings),
            ),
        ),
        open=DropdownDialogState.which_dialog_open == "settings",
        on_open_change=DropdownDialogState.set_which_dialog_open(""),
    ),
    align="center",
)

Filling the order

The '⋯' button at the end of each row gets a menu. Moving and changing the status are grouped into submenus, and the chosen value goes into State through on_select to update the text in the row. The button sits at the right end of the row, so the panel is lined up with align="end".

Q3 report

Inbox · Draft

class DropdownRowState(rx.State):
    status: str = "Draft"
    folder: str = "Inbox"

    @rx.event
    def set_status(self, value: str):
        self.status = value

    @rx.event
    def move(self, folder: str):
        self.folder = folder

    @rx.event
    def duplicate(self):
        return rx.toast("Duplicated Q3 report")


rx.flex(
    rx.icon("file-text", color=rx.color("gray", 10)),
    rx.box(
        rx.text("Q3 report", weight="bold"),
        rx.text(DropdownRowState.folder, " · ", DropdownRowState.status, size="1", color_scheme="gray"),
    ),
    rx.spacer(),
    rx.menu.root(
        rx.menu.trigger(rx.icon_button(rx.icon("ellipsis"), variant="ghost", color_scheme="gray", aria_label="More actions")),
        rx.menu.content(
            rx.menu.item("Duplicate", shortcut="⌘ D", on_select=DropdownRowState.duplicate),
            rx.menu.sub(
                rx.menu.sub_trigger("Move to"),
                rx.menu.sub_content(
                    rx.menu.item("Inbox", on_select=DropdownRowState.move("Inbox")),
                    rx.menu.item("Projects", on_select=DropdownRowState.move("Projects")),
                    rx.menu.item("Archive", on_select=DropdownRowState.move("Archive")),
                ),
            ),
            rx.menu.sub(
                rx.menu.sub_trigger("Status"),
                rx.menu.sub_content(
                    rx.menu.item("Draft", on_select=DropdownRowState.set_status("Draft")),
                    rx.menu.item("Published", on_select=DropdownRowState.set_status("Published")),
                ),
            ),
            rx.menu.separator(),
            rx.menu.item("Delete", shortcut="⌘ ⌫", color_scheme="red", disabled=True),
            align="end",
        ),
    ),
    spacing="3",
    align="center",
    padding="8px 12px",
    border=f"1px solid {rx.color('gray', 5)}",
    border_radius="8px",
    max_width="420px",
)

Workshop notes

  • rx.menu = rx.dropdown_menu. root > trigger + content > item · separator · sub(sub_trigger, sub_content).

  • Choosing an item fires on_select and closes the menu. Pass the value as a handler argument.

  • shortcut only displays text. The actual keyboard shortcut has to be built separately.

  • Look and placement go on content (variant, size, color_scheme, side, align).

  • To open a dialog from a menu, keep the dialog outside the menu and open it through State.

← Previous

Overlay · Drawer

Next →

Overlay · Context Menu

Get news about new lessons

We'll email you when this course gets new lessons or a follow-up course opens.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

R

ReflexKR

Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.

ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee

Affiliate disclosure
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex