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

Popover

rx.popover.rootrx.popover.triggerrx.popover.contentrx.popover.close

Official docs

Workshop order

When someone clicks the 'Comment' button under a post, open a small panel right there so they can write a short comment without leaving the page.

rx.popover is a small panel that pops up next to a button when you click it. Unlike a tooltip, it opens on click, and it can hold things to interact with, such as inputs and buttons. It is built from four parts.

  • popover.root: wraps all the parts of the popover.
  • popover.trigger: wraps the button that opens and closes the popover.
  • popover.content: the content that pops up when it is open.
  • popover.close: wraps a button that closes the popover.
rx.popover.root(
    rx.popover.trigger(
        rx.button("Popover"),
    ),
    rx.popover.content(
        rx.flex(
            rx.text("Simple Example"),
            rx.popover.close(
                rx.button("Close"),
            ),
            direction="column",
            spacing="3",
        ),
    ),
)

Try the props

Placement and size go on popover.content; whether it is modal goes on popover.root. Change a value, then click Open to see it.

Try the props

text

size

side

align

modal

rx.popover.root(
    rx.popover.trigger(rx.button("Open")),
    rx.popover.content(
        rx.flex(
            rx.text("Simple Example"),
            rx.popover.close(rx.button("Close", size="1")),
            direction="column",
            spacing="3",
        ),
        size="2",
        side="bottom",
        align="center",
    ),
    modal=False,
)
size changes the padding and the corner radius together. With modal on, nothing outside can be clicked or scrolled while it is open.
PartPropValuesUse
rootopenTrue / FalseDrive the open state from State (controlled); pair with on_open_change
rootdefault_openTrue / FalseOpen on first render (uncontrolled)
rootmodalTrue / FalseBlock interaction outside while open and hide the outside from screen readers
rooton_open_changeeventFires on open or close; passes the new open value (bool)
contentsize"1" ~ "4"Padding and corner size
contentside · alignposition nameSide it opens on and where it lines up (default "bottom", "center")
contentside_offset · align_offsetnumber (px)Gap from the button; extra shift from the aligned position
contentavoid_collisions · collision_paddingbool · numberMove away from the screen edge, and the margin for that
contentsticky · hide_when_detached"partial" "always" · boolHow closely it sticks while scrolling; hide when the button is covered
contenton_open_auto_focus · on_close_auto_focuseventFocus moves inside on open, back to the button on close
contenton_escape_key_down · on_pointer_down_outsideeventEsc key; a press outside
contenton_focus_outside · on_interact_outsideeventFocus or interaction moves outside

trigger and close have no props of their own; they only take the common events (on_click and so on).

In practice: a comment panel

A popover can hold anything. Here is a comment panel with an input, a checkbox and a send button next to an avatar. Wrapping the send button in popover.close closes the panel the moment it is clicked. Set the width directly on content, as in style={"width": 360}.

rx.popover.root(
    rx.popover.trigger(
        rx.button("Comment", variant="soft"),
    ),
    rx.popover.content(
        rx.flex(
            rx.avatar("2", fallback="RX", radius="full"),
            rx.box(
                rx.text_area(placeholder="Write a comment…", style={"height": 80}),
                rx.flex(
                    rx.checkbox("Send to group"),
                    rx.popover.close(rx.button("Comment", size="1")),
                    spacing="3",
                    margin_top="12px",
                    justify="between",
                ),
                flex_grow="1",
            ),
            spacing="3",
        ),
        style={"width": 360},
    ),
)

Put rx.inset(side="top") as the first child of content to add a header image that runs all the way to the edges, with no top padding.

rx.popover.root(
    rx.popover.trigger(
        rx.button("Feedback", variant="classic"),
    ),
    rx.popover.content(
        rx.inset(
            side="top",
            background="url('https://picsum.photos/id/29/720/200') center/cover",
            height="100px",
        ),
        rx.box(
            rx.text_area(placeholder="Write a comment…", style={"height": 80}),
            rx.flex(
                rx.checkbox("Send to group"),
                rx.popover.close(rx.button("Comment", size="1")),
                spacing="3",
                margin_top="12px",
                justify="between",
            ),
            padding_top="12px",
        ),
        style={"width": 360},
    ),
)

A State value as the trigger label

To use a State value as the button label, wrap it once in rx.text, as in rx.button(rx.text(State.value)). The official docs note that passing it directly, as in rx.button(State.value), does not work as expected. Pick a language below and the button label changes.

class PopoverLanguageState(rx.State):
    language: str = "EN"

    @rx.event
    def set_language(self, value: str):
        self.language = value


rx.popover.root(
    rx.popover.trigger(
        rx.button(rx.text(PopoverLanguageState.language), variant="outline"),
    ),
    rx.popover.content(
        rx.flex(
            rx.popover.close(rx.button("EN", size="1", variant="soft", on_click=PopoverLanguageState.set_language("EN"))),
            rx.popover.close(rx.button("KO", size="1", variant="soft", on_click=PopoverLanguageState.set_language("KO"))),
            rx.popover.close(rx.button("JA", size="1", variant="soft", on_click=PopoverLanguageState.set_language("JA"))),
            spacing="2",
        ),
    ),
)

Reacting to open and close: on_open_change

The on_open_change of popover.root passes True to the handler when it opens and False when it closes.

Opened or closed: 0 times

Popover open: no

class PopoverEventState(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: ", PopoverEventState.num_opens, " times"),
    rx.text("Popover open: ", rx.cond(PopoverEventState.opened, "yes", "no")),
    rx.popover.root(
        rx.popover.trigger(
            rx.button("Popover"),
        ),
        rx.popover.content(
            rx.flex(
                rx.text("Simple Example"),
                rx.popover.close(
                    rx.button("Close"),
                ),
                direction="column",
                spacing="3",
            ),
        ),
        on_open_change=PopoverEventState.count_opens,
    ),
    direction="column",
    spacing="3",
    align="start",
)

Opening and closing from State

Wire up open and on_open_change together and the pencil button and outside clicks work as usual, while your code can also close the panel. Below, change the name and click Rename: the handler saves the value and closes the panel with self.open = False. Each time it opens, the input is refilled with the current name.

Untitled

class PopoverControlState(rx.State):
    open: bool = False
    name: str = "Untitled"
    draft: str = ""

    @rx.event
    def set_open(self, value: bool):
        self.open = value
        if value:
            self.draft = self.name

    @rx.event
    def set_draft(self, value: str):
        self.draft = value

    @rx.event
    def rename(self):
        self.name = self.draft.strip() or "Untitled"
        self.open = False


rx.flex(
    rx.heading(PopoverControlState.name, size="4"),
    rx.popover.root(
        rx.popover.trigger(rx.icon_button(rx.icon("pencil"), variant="soft", size="1", aria_label="Rename")),
        rx.popover.content(
            rx.flex(
                rx.input(value=PopoverControlState.draft, on_change=PopoverControlState.set_draft),
                rx.button("Rename", size="1", on_click=PopoverControlState.rename),
                spacing="2",
            ),
        ),
        open=PopoverControlState.open,
        on_open_change=PopoverControlState.set_open,
    ),
    align="center",
    spacing="3",
)

Filling the order

A small comment panel that opens from the 'Comment' button under a post. Set side="top" so it opens upward and align="start" so it lines up with the left edge of the button.

Shipping is now free over $30.

rx.card(
    rx.text("Shipping is now free over $30.", size="2"),
    rx.popover.root(
        rx.popover.trigger(rx.button(rx.icon("message-circle"), "Comment", variant="ghost", size="1", margin_top="8px")),
        rx.popover.content(
            rx.flex(
                rx.text_area(placeholder="Write a comment…", rows="3"),
                rx.flex(
                    rx.popover.close(rx.button("Cancel", size="1", variant="soft", color_scheme="gray")),
                    rx.popover.close(rx.button("Post", size="1")),
                    spacing="2",
                    justify="end",
                ),
                direction="column",
                spacing="2",
            ),
            side="top",
            align="start",
            width="300px",
        ),
    ),
    max_width="360px",
)

Workshop notes

  • Four parts: root · trigger · content · close. Clicking trigger opens it; clicking close or anywhere outside closes it.

  • Placement and size go on content (side, align, side_offset, size); modal goes on root (modal).

  • To use a State value as the trigger button label, wrap it in rx.text.

  • on_open_change passes the new open value (bool). Control it with open and a handler can close the panel.

← Previous

Overlay · Tooltip

Next →

Overlay · Hover Card

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