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

Context Menu

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

Official docs

Workshop order

When someone right-clicks a file name in the file list, open a menu right there for starring, duplicating or deleting it.

A context menu opens at the pointer when you right-click an element (or long-press it on a touch screen). Use it to collect the things you can do "with this one", such as a file, a table row or an image. Its parts are the same as the dropdown menu's.

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

Try the props

Right-click the dashed box. The look of the menu panel goes on content, and disabled, which blocks the right-click, goes on trigger. With disabled on, the browser's own right-click menu appears instead.

Try the props

Right-click here

variant

size

color

high_contrast

disabled

modal

rx.context_menu.root(
    rx.context_menu.trigger(
        rx.center(
            rx.text("Right-click here", size="2", color_scheme="gray"),
            width="260px",
            height="110px",
            border=f"2px dashed {rx.color('gray', 7)}",
            border_radius="8px",
        ),
        disabled=False,
    ),
    rx.context_menu.content(
        rx.context_menu.item("Copy", shortcut="⌘ C"),
        rx.context_menu.item("Paste", shortcut="⌘ V"),
        rx.context_menu.separator(),
        rx.context_menu.item("Delete", shortcut="⌘ ⌫", color_scheme="red"),
        variant="solid",
        size="2",
        color_scheme="indigo",
        high_contrast=False,
    ),
    modal=True,
)
A context menu opens where the pointer was pressed, so you will mostly change variant, size and color_scheme rather than side and align.
PartPropValuesUse
rootmodalTrue / FalseBlock interaction outside while open
rooton_open_changeeventFires on open or close; passes the new open value (bool)
triggerdisabledTrue / FalseDon't open on right-click
contentvariant · size"solid" "soft" · "1" "2"Highlight style, size
contentcolor_scheme · high_contrastcolor name · boolColor used for items, contrast
contentloop · side · align · side_offset · align_offset-Keyboard wrap-around, fine-tuning the position
contentavoid_collisions · collision_padding · sticky · hide_when_detached-Screen-edge handling
itemshortcut · color_scheme · disabled · text_value-Shortcut text, color, make it unselectable, text for typeahead
itemon_selecteventAn item is chosen
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

Unlike the dropdown menu, root has no open: the menu only opens on right-click. content and sub_content have the on_escape_key_down, on_pointer_down_outside, on_focus_outside and on_interact_outside events.

Items go directly under content

context_menu.item must be a direct child of context_menu.content. If you group items in a box such as rx.vstack to switch them on a condition, the menu no longer recognizes them. This is the form the official docs warn against.

rx.context_menu.content(
    rx.cond(
        State.count % 2 == 0,
        rx.vstack(  # don't wrap items in a box
            rx.context_menu.item("Even Option 1"),
            rx.context_menu.item("Even Option 2"),
        ),
        rx.vstack(
            rx.context_menu.item("Odd Option A"),
            rx.context_menu.item("Odd Option B"),
        ),
    )
)

To change the items based on State, make the list a computed var and draw the items directly with rx.foreach, which does not add a box of its own. Click the count button below, then right-click again.

Selected: none

class ContextMenuOptionsState(rx.State):
    count: int = 0
    selected: str = "none"

    @rx.event
    def increment(self):
        self.count += 1

    @rx.event
    def set_selected(self, value: str):
        self.selected = value

    @rx.var
    def options(self) -> list[str]:
        if self.count % 2 == 0:
            return ["Even Option 1", "Even Option 2", "Even Option 3"]
        return ["Odd Option A", "Odd Option B", "Odd Option C"]


rx.flex(
    rx.context_menu.root(
        rx.context_menu.trigger(rx.button("Right click me")),
        rx.context_menu.content(
            rx.foreach(
                ContextMenuOptionsState.options,
                lambda option: rx.context_menu.item(option, on_select=ContextMenuOptionsState.set_selected(option)),
            ),
        ),
    ),
    rx.button("count = ", ContextMenuOptionsState.count, variant="soft", on_click=ContextMenuOptionsState.increment),
    rx.text("Selected: ", ContextMenuOptionsState.selected, size="2"),
    spacing="3",
    align="center",
    wrap="wrap",
)

Reacting to open and close: on_open_change

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

Opened or closed: 0 times

Context menu open: no

class ContextMenuEventState(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: ", ContextMenuEventState.num_opens, " times"),
    rx.text("Context menu open: ", rx.cond(ContextMenuEventState.opened, "yes", "no")),
    rx.context_menu.root(
        rx.context_menu.trigger(rx.button("Right click me", variant="soft")),
        rx.context_menu.content(
            rx.context_menu.item("Copy"),
            rx.context_menu.item("Paste"),
        ),
        on_open_change=ContextMenuEventState.count_opens,
    ),
    direction="column",
    spacing="3",
    align="start",
)

Opening a dialog from the menu

As with the dropdown menu, a dialog placed inside a menu item disappears when the menu closes. Keep the dialogs outside the menu and let the items change only a State value. Each dialog opens when that value is its own name, and on_open_change clears the value when it closes. Right-click the icon below.

class ContextMenuDialogState(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.context_menu.root(
        rx.context_menu.trigger(rx.icon_button(rx.icon("ellipsis-vertical"), variant="soft", aria_label="Right-click for menu")),
        rx.context_menu.content(
            rx.context_menu.item("Delete", on_click=ContextMenuDialogState.set_which_dialog_open("delete")),
            rx.context_menu.item("Settings", on_click=ContextMenuDialogState.set_which_dialog_open("settings")),
        ),
    ),
    rx.cond(
        ContextMenuDialogState.which_dialog_open,
        rx.text(ContextMenuDialogState.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=ContextMenuDialogState.delete),
                ),
                rx.spacer(),
                rx.alert_dialog.cancel(rx.button("Cancel")),
            ),
        ),
        open=ContextMenuDialogState.which_dialog_open == "delete",
        on_open_change=ContextMenuDialogState.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=ContextMenuDialogState.save_settings),
            ),
        ),
        open=ContextMenuDialogState.which_dialog_open == "settings",
        on_open_change=ContextMenuDialogState.set_which_dialog_open(""),
    ),
    align="center",
)

Filling the order

Draw each file row with rx.foreach and wrap every row in a context menu. Each item's on_select passes that file's name, so a single handler works from any row.

notes.txt

budget.xlsx

photo.png

class ContextMenuFileState(rx.State):
    files: list[str] = ["notes.txt", "budget.xlsx", "photo.png"]
    starred: list[str] = []

    @rx.event
    def toggle_star(self, name: str):
        if name in self.starred:
            self.starred = [f for f in self.starred if f != name]
        else:
            self.starred = self.starred + [name]

    @rx.event
    def duplicate(self, name: str):
        stem, dot, ext = name.rpartition(".")
        self.files = self.files + [f"{stem} copy{dot}{ext}"]

    @rx.event
    def remove(self, name: str):
        self.files = [f for f in self.files if f != name]
        self.starred = [f for f in self.starred if f != name]

    @rx.event
    def reset_files(self):
        self.files = ["notes.txt", "budget.xlsx", "photo.png"]
        self.starred = []


rx.vstack(
    rx.foreach(
        ContextMenuFileState.files,
        lambda name: rx.context_menu.root(
            rx.context_menu.trigger(
                rx.flex(
                    rx.icon("file"),
                    rx.text(name),
                    rx.spacer(),
                    rx.cond(ContextMenuFileState.starred.contains(name), rx.icon("star", color=rx.color("amber", 9))),
                    spacing="2",
                    align="center",
                    width="100%",
                    padding="6px 10px",
                    border_radius="6px",
                    _hover={"background": rx.color("gray", 3)},
                ),
            ),
            rx.context_menu.content(
                rx.context_menu.item("Star / unstar", on_select=ContextMenuFileState.toggle_star(name)),
                rx.context_menu.item("Duplicate", shortcut="⌘ D", on_select=ContextMenuFileState.duplicate(name)),
                rx.context_menu.separator(),
                rx.context_menu.item("Delete", shortcut="⌘ ⌫", color_scheme="red", on_select=ContextMenuFileState.remove(name)),
            ),
        ),
    ),
    rx.button("Reset", size="1", variant="soft", on_click=ContextMenuFileState.reset_files),
    width="100%",
    max_width="320px",
    spacing="1",
)

Workshop notes

  • Opens on right-click (long-press). Same parts as the dropdown menu: root · trigger · content · item · separator · sub.

  • item must be a direct child of content. Draw dynamic items directly with rx.foreach.

  • root has no open. Use on_open_change if you need to know whether it is open.

  • With disabled=True on trigger, the browser's own menu appears.

  • Right-click is an invisible feature. Offer the same actions through a button or a dropdown menu as well.

← Previous

Overlay · Dropdown Menu

Next →

Overlay · Toast

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