Skip to content
This repository was archived by the owner on Apr 23, 2026. It is now read-only.

Commit 4ee06b5

Browse files
Add preview card component from Base UI
- Create PreviewCard component with all sub-components (Root, Trigger, Portal, Positioner, Popup, Arrow, Backdrop) - Implement HighLevelPreviewCard wrapper with trigger and content props as requested - Follow existing component patterns from popover and tooltip components - Add component to lazy loading system in __init__.py - Add demo usage in demo application - All prop descriptions sourced from official Base UI documentation Co-Authored-By: Carlos Cutillas <carlos@reflex.dev>
1 parent 1e04e20 commit 4ee06b5

3 files changed

Lines changed: 288 additions & 0 deletions

File tree

demo/demo/demo.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ def index() -> rx.Component:
5858
on_value_change=lambda value: rx.toast.success(f"Value: {value}"),
5959
on_open_change=lambda value: rx.toast.success(f"Open: {value}"),
6060
),
61+
ui.preview_card(
62+
trigger=ui.button("Hover for preview", variant="outline"),
63+
content=rx.el.div(
64+
rx.el.h3("Preview Card", class_name="text-lg font-semibold mb-2"),
65+
rx.el.p("This is a preview card component that shows content on hover.", class_name="text-sm text-secondary-11"),
66+
class_name="p-4"
67+
),
68+
delay=300,
69+
close_delay=100,
70+
),
6171
ui.theme_switcher(class_name="absolute top-4 right-4"),
6272
class_name=ui.cn(
6373
"flex flex-col gap-6 items-center justify-center h-screen", "bg-secondary-1"

reflex_ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"components.base.menu": ["menu"],
1616
"components.base.navigation_menu": ["navigation_menu"],
1717
"components.base.popover": ["popover"],
18+
"components.base.preview_card": ["preview_card"],
1819
"components.base.scroll_area": ["scroll_area"],
1920
"components.base.select": ["select"],
2021
"components.base.skeleton": ["skeleton"],
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
"""Custom preview card component."""
2+
3+
from typing import Literal
4+
5+
from reflex.components.component import Component, ComponentNamespace
6+
from reflex.event import EventHandler, passthrough_event_spec
7+
from reflex.utils.imports import ImportVar
8+
from reflex.vars.base import Var
9+
10+
from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent
11+
from reflex_ui.utils.twmerge import cn
12+
13+
LiteralAlign = Literal["start", "center", "end"]
14+
LiteralSide = Literal["bottom", "inline-end", "inline-start", "left", "right", "top"]
15+
LiteralPosition = Literal["absolute", "fixed"]
16+
17+
18+
class ClassNames:
19+
"""Class names for preview card components."""
20+
21+
ROOT = ""
22+
TRIGGER = ""
23+
BACKDROP = ""
24+
PORTAL = ""
25+
POSITIONER = ""
26+
POPUP = "origin-(--transform-origin) rounded-xl p-4 border border-secondary-a4 bg-secondary-1 shadow-large transition-[transform,scale,opacity] data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 outline-none min-w-64 flex flex-col gap-3"
27+
ARROW = "data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180"
28+
29+
30+
class PreviewCardBaseComponent(BaseUIComponent):
31+
"""Base component for preview card components."""
32+
33+
library = f"{PACKAGE_NAME}/preview-card"
34+
35+
@property
36+
def import_var(self):
37+
"""Return the import variable for the preview card component."""
38+
return ImportVar(tag="PreviewCard", package_path="", install=False)
39+
40+
41+
class PreviewCardRoot(PreviewCardBaseComponent):
42+
"""Groups all parts of the preview card. Doesn't render its own HTML element."""
43+
44+
tag = "PreviewCard.Root"
45+
46+
default_open: Var[bool]
47+
48+
open: Var[bool]
49+
50+
on_open_change: EventHandler[passthrough_event_spec(bool, dict, str)]
51+
52+
# Event handler called after any animations complete when the preview card is opened or closed.
53+
on_open_change_complete: EventHandler[passthrough_event_spec(bool)]
54+
55+
delay: Var[int]
56+
57+
# How long to wait before closing the preview card that was opened on hover. Specified in milliseconds. Defaults to 300.
58+
close_delay: Var[int]
59+
60+
@classmethod
61+
def create(cls, *children, **props) -> BaseUIComponent:
62+
"""Create the preview card root component."""
63+
props["data-slot"] = "preview-card"
64+
return super().create(*children, **props)
65+
66+
67+
class PreviewCardTrigger(PreviewCardBaseComponent):
68+
"""A button that opens the preview card. Renders a <button> element."""
69+
70+
tag = "PreviewCard.Trigger"
71+
72+
# The render prop
73+
render_: Var[Component]
74+
75+
@classmethod
76+
def create(cls, *children, **props) -> BaseUIComponent:
77+
"""Create the preview card trigger component."""
78+
props["data-slot"] = "preview-card-trigger"
79+
cls.set_class_name(ClassNames.TRIGGER, props)
80+
return super().create(*children, **props)
81+
82+
83+
class PreviewCardBackdrop(PreviewCardBaseComponent):
84+
"""An overlay displayed beneath the popup. Renders a <div> element."""
85+
86+
tag = "PreviewCard.Backdrop"
87+
88+
# The render prop
89+
render_: Var[Component]
90+
91+
@classmethod
92+
def create(cls, *children, **props) -> BaseUIComponent:
93+
"""Create the preview card backdrop component."""
94+
props["data-slot"] = "preview-card-backdrop"
95+
cls.set_class_name(ClassNames.BACKDROP, props)
96+
return super().create(*children, **props)
97+
98+
99+
class PreviewCardPortal(PreviewCardBaseComponent):
100+
"""A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>."""
101+
102+
tag = "PreviewCard.Portal"
103+
104+
# A parent element to render the portal element into.
105+
container: Var[str]
106+
107+
# Whether to keep the portal mounted in the DOM while the popup is hidden. Defaults to False.
108+
keep_mounted: Var[bool]
109+
110+
111+
class PreviewCardPositioner(PreviewCardBaseComponent):
112+
"""Positions the preview card against the trigger. Renders a <div> element."""
113+
114+
tag = "PreviewCard.Positioner"
115+
116+
# How to align the popup relative to the specified side. Defaults to "center".
117+
align: Var[LiteralAlign]
118+
119+
# Additional offset along the alignment axis in pixels. Defaults to 0.
120+
align_offset: Var[int]
121+
122+
# Which side of the anchor element to align the popup against. May automatically change to avoid collisions. Defaults to "bottom".
123+
side: Var[LiteralSide]
124+
125+
# Distance between the anchor and the popup in pixels. Defaults to 0.
126+
side_offset: Var[int]
127+
128+
# Minimum distance to maintain between the arrow and the edges of the popup. Use it to prevent the arrow element from hanging out of the rounded corners of a popup. Defaults to 5.
129+
arrow_padding: Var[int]
130+
131+
# An element to position the popup against. By default, the popup will be positioned against the trigger.
132+
anchor: Var[str]
133+
134+
# An element or a rectangle that delimits the area that the popup is confined to. Defaults to "clipping-ancestors".
135+
collision_boundary: Var[str]
136+
137+
# Additional space to maintain from the edge of the collision boundary. Defaults to 5.
138+
collision_padding: Var[int | list[int]]
139+
140+
# Whether to maintain the popup in the viewport after the anchor element was scrolled out of view. Defaults to False.
141+
sticky: Var[bool]
142+
143+
# Determines which CSS position property to use. Defaults to "absolute".
144+
position_method: Var[LiteralPosition]
145+
146+
# Whether the popup tracks any layout shift of its positioning anchor. Defaults to True.
147+
track_anchor: Var[bool]
148+
149+
# Determines how to handle collisions when positioning the popup.
150+
collision_avoidance: Var[str]
151+
152+
# The render prop
153+
render_: Var[Component]
154+
155+
@classmethod
156+
def create(cls, *children, **props) -> BaseUIComponent:
157+
"""Create the preview card positioner component."""
158+
props["data-slot"] = "preview-card-positioner"
159+
props.setdefault("side_offset", 4)
160+
cls.set_class_name(ClassNames.POSITIONER, props)
161+
return super().create(*children, **props)
162+
163+
164+
class PreviewCardPopup(PreviewCardBaseComponent):
165+
"""A container for the preview card contents. Renders a <div> element."""
166+
167+
tag = "PreviewCard.Popup"
168+
169+
# The render prop
170+
render_: Var[Component]
171+
172+
@classmethod
173+
def create(cls, *children, **props) -> BaseUIComponent:
174+
"""Create the preview card popup component."""
175+
props["data-slot"] = "preview-card-popup"
176+
cls.set_class_name(ClassNames.POPUP, props)
177+
return super().create(*children, **props)
178+
179+
180+
class PreviewCardArrow(PreviewCardBaseComponent):
181+
"""Displays an element positioned against the preview card anchor. Renders a <div> element."""
182+
183+
tag = "PreviewCard.Arrow"
184+
185+
# The render prop
186+
render_: Var[Component]
187+
188+
@classmethod
189+
def create(cls, *children, **props) -> BaseUIComponent:
190+
"""Create the preview card arrow component."""
191+
props["data-slot"] = "preview-card-arrow"
192+
cls.set_class_name(ClassNames.ARROW, props)
193+
return super().create(*children, **props)
194+
195+
196+
class HighLevelPreviewCard(PreviewCardRoot):
197+
"""High level wrapper for the PreviewCard component."""
198+
199+
trigger: Var[Component | None]
200+
content: Var[str | Component | None]
201+
202+
# Props for different component parts
203+
_positioner_props = {
204+
"align",
205+
"align_offset",
206+
"side",
207+
"side_offset",
208+
"arrow_padding",
209+
"collision_padding",
210+
"collision_boundary",
211+
"sticky",
212+
"position_method",
213+
"track_anchor",
214+
"anchor",
215+
"collision_avoidance",
216+
}
217+
_portal_props = {"container", "keep_mounted"}
218+
219+
@classmethod
220+
def create(cls, *children, **props) -> BaseUIComponent:
221+
"""Create a preview card component.
222+
223+
Args:
224+
*children: Additional children to include in the preview card.
225+
**props: Additional properties to apply to the preview card component.
226+
227+
Returns:
228+
The preview card component.
229+
"""
230+
# Extract props for different parts
231+
positioner_props = {
232+
k: props.pop(k) for k in cls._positioner_props & props.keys()
233+
}
234+
portal_props = {k: props.pop(k) for k in cls._portal_props & props.keys()}
235+
236+
trigger = props.pop("trigger", None)
237+
content = props.pop("content", None)
238+
class_name = props.pop("class_name", "")
239+
240+
return PreviewCardRoot.create(
241+
PreviewCardTrigger.create(render_=trigger) if trigger else None,
242+
PreviewCardPortal.create(
243+
PreviewCardPositioner.create(
244+
PreviewCardPopup.create(
245+
content,
246+
*children,
247+
class_name=cn(ClassNames.POPUP, class_name),
248+
),
249+
**positioner_props,
250+
),
251+
**portal_props,
252+
),
253+
**props,
254+
)
255+
256+
def _exclude_props(self) -> list[str]:
257+
return [
258+
*super()._exclude_props(),
259+
"trigger",
260+
"content",
261+
]
262+
263+
264+
class PreviewCard(ComponentNamespace):
265+
"""Namespace for PreviewCard components."""
266+
267+
root = staticmethod(PreviewCardRoot.create)
268+
trigger = staticmethod(PreviewCardTrigger.create)
269+
backdrop = staticmethod(PreviewCardBackdrop.create)
270+
portal = staticmethod(PreviewCardPortal.create)
271+
positioner = staticmethod(PreviewCardPositioner.create)
272+
popup = staticmethod(PreviewCardPopup.create)
273+
arrow = staticmethod(PreviewCardArrow.create)
274+
__call__ = staticmethod(HighLevelPreviewCard.create)
275+
276+
277+
preview_card = PreviewCard()

0 commit comments

Comments
 (0)