reflex-react-joyride 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. reflex_react_joyride-0.1.0/LICENSE +21 -0
  2. reflex_react_joyride-0.1.0/PKG-INFO +341 -0
  3. reflex_react_joyride-0.1.0/README.md +313 -0
  4. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/__init__.py +81 -0
  5. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/actions.py +104 -0
  6. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/constants.py +188 -0
  7. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/events.py +73 -0
  8. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/joyride.py +282 -0
  9. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/joyride.pyi +149 -0
  10. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/joyride_tour.jsx +277 -0
  11. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/props.py +252 -0
  12. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride/py.typed +0 -0
  13. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/PKG-INFO +341 -0
  14. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/SOURCES.txt +21 -0
  15. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/dependency_links.txt +1 -0
  16. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/requires.txt +1 -0
  17. reflex_react_joyride-0.1.0/custom_components/reflex_react_joyride.egg-info/top_level.txt +1 -0
  18. reflex_react_joyride-0.1.0/pyproject.toml +85 -0
  19. reflex_react_joyride-0.1.0/setup.cfg +4 -0
  20. reflex_react_joyride-0.1.0/tests/test_actions.py +49 -0
  21. reflex_react_joyride-0.1.0/tests/test_component.py +122 -0
  22. reflex_react_joyride-0.1.0/tests/test_constants.py +28 -0
  23. reflex_react_joyride-0.1.0/tests/test_props.py +64 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ernesto Crespo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,341 @@
1
+ Metadata-Version: 2.4
2
+ Name: reflex-react-joyride
3
+ Version: 0.1.0
4
+ Summary: Reflex custom component for react-joyride: guided product tours and onboarding walkthroughs, with controlled mode, theming, custom tooltips and Python-driven controls
5
+ Author-email: Ernesto Crespo <ecrespo@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ecrespo/reflex-react-joyride
8
+ Project-URL: Source, https://github.com/ecrespo/reflex-react-joyride
9
+ Project-URL: Issues, https://github.com/ecrespo/reflex-react-joyride/issues
10
+ Keywords: reflex,reflex-custom-components,react-joyride,joyride,tour,onboarding,walkthrough,product-tour
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Software Development :: User Interfaces
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: reflex>=0.9.11
27
+ Dynamic: license-file
28
+
29
+ # reflex-react-joyride
30
+
31
+ Guided product tours and onboarding walkthroughs for [Reflex](https://reflex.dev), powered by [react-joyride](https://react-joyride.com) v3.
32
+
33
+ - Wraps **react-joyride 3.2.0** through its `useJoyride` hook, so the tour's controls are reachable from Python
34
+ - Typed props for everything: `Step`, `TourOptions`, `Locale`, `TourStyles`, `FloatingOptions` — `snake_case` in Python, `camelCase` in the browser
35
+ - **Uncontrolled** (`run`) or **controlled** (`step_index`) mode, whichever fits your State
36
+ - **Python actions**: `start`, `next_step`, `prev_step`, `go_to`, `skip`, `reset`, `replay`, `stop`, `open_tooltip`, `get_state`
37
+ - One catch-all `on_event` plus a dedicated trigger per event (`on_tour_start`, `on_step_after`, `on_target_not_found`, ...) with a flat, JSON-safe payload
38
+ - **Custom tooltip and beacon** written as ordinary Reflex components
39
+ - Step content and titles accept Reflex components, not just strings
40
+ - Constants mirroring react-joyride's literals: `EVENTS`, `ACTIONS`, `STATUS`, `LIFECYCLE`, `ORIGIN`
41
+
42
+ ## Screenshots
43
+
44
+ The four pages of the demo app, in order:
45
+
46
+ | | |
47
+ | --- | --- |
48
+ | ![Basic tour](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/1-tour-basico.png) | ![Controlled mode](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/2-modo-controlado.png) |
49
+ | A continuous tour over a small dashboard, with the event log filling up | Controlled mode: the step index lives in the State and external buttons drive it |
50
+ | ![Spanish locale and theming](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/3-tema-locale-es.png) | ![Custom Reflex tooltip](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/4-tooltip-reflex.png) |
51
+ | `locale`, `options` and `styles` restyling the built-in tooltip | The same step rendered by a Reflex component through the `tooltip` render prop |
52
+ | ![Modal step](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/5-paso-modal.png) | ![Spotlight target](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/6-spotlight-y-targets.png) |
53
+ | `placement="center"` on `body` for a modal-like opening step | A spotlight over a different element than the one the tooltip points at |
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install reflex-react-joyride
59
+ # or
60
+ uv add reflex-react-joyride
61
+ ```
62
+
63
+ ## Quick start
64
+
65
+ ```python
66
+ import reflex as rx
67
+ import reflex_react_joyride as rjr
68
+
69
+
70
+ class TourState(rx.State):
71
+ run: bool = False
72
+
73
+ @rx.event
74
+ def start(self):
75
+ self.run = True
76
+
77
+ @rx.event
78
+ def finished(self, event: rjr.JoyrideEvent):
79
+ self.run = False
80
+
81
+
82
+ def index() -> rx.Component:
83
+ return rx.vstack(
84
+ rjr.joyride(
85
+ tour_id="welcome",
86
+ run=TourState.run,
87
+ continuous=True,
88
+ steps=[
89
+ rjr.Step(target="#logo", title="Welcome", content="This is the app."),
90
+ rjr.Step(target="#cta", content="Start here.", placement="top"),
91
+ ],
92
+ options=rjr.TourOptions(show_progress=True, primary_color="#7c3aed"),
93
+ on_tour_end=TourState.finished,
94
+ ),
95
+ rx.heading("Acme", id="logo"),
96
+ rx.button("Get started", id="cta"),
97
+ rx.button("Take the tour", on_click=TourState.start),
98
+ )
99
+ ```
100
+
101
+ ## Steps
102
+
103
+ A step needs a `target` (any CSS selector) and some `content`. Everything in
104
+ `TourOptions` can be overridden per step:
105
+
106
+ ```python
107
+ rjr.Step(
108
+ target="#sidebar",
109
+ title="Navigation",
110
+ content=rx.vstack(rx.text("Every section lives here."), rx.code("Cmd+K")),
111
+ placement="right", # top | bottom | left | right | auto | center (+ -start / -end)
112
+ spotlight_padding=16,
113
+ show_progress=True,
114
+ id="sidebar", # echoed back as event["step_id"]
115
+ data={"section": "nav"}, # echoed back as event["step_data"], keys untouched
116
+ )
117
+ ```
118
+
119
+ `target="body"` with `placement="center"` gives a modal-like step, which is a
120
+ good opener. Plain dicts work as well as `Step` objects, and so do steps kept in
121
+ a State var:
122
+
123
+ ```python
124
+ class State(rx.State):
125
+ steps: list[dict] = [{"target": "#logo", "content": "Hi", "show_progress": True}]
126
+
127
+
128
+ rjr.joyride(steps=State.steps)
129
+ ```
130
+
131
+ `snake_case` keys are converted to the `camelCase` react-joyride expects, both
132
+ for literal props (in Python) and for values coming from the State (in the
133
+ browser), so you never have to write `spotlightPadding` yourself. Keys that are
134
+ already `camelCase` pass through untouched.
135
+
136
+ ## Theming
137
+
138
+ `options` sets behaviour and colors for the whole tour, `styles` overrides the
139
+ CSS of every element react-joyride renders, and `locale` replaces the strings:
140
+
141
+ ```python
142
+ rjr.joyride(
143
+ steps=STEPS,
144
+ options=rjr.TourOptions(
145
+ primary_color="#059669",
146
+ background_color="#f2fbf7",
147
+ overlay_color="rgba(4, 47, 36, 0.55)",
148
+ show_progress=True,
149
+ skip_beacon=True, # go straight to the tooltip, no beacon
150
+ spotlight_radius=12,
151
+ buttons=["back", "skip", "primary"],
152
+ width=420,
153
+ z_index=1000,
154
+ ),
155
+ styles=rjr.TourStyles(
156
+ tooltip={"border_radius": "14px", "box_shadow": "0 18px 40px rgba(15, 23, 42, 0.18)"},
157
+ button_primary={"border_radius": "999px", "padding": "8px 18px"},
158
+ ),
159
+ locale=rjr.Locale(
160
+ back="Atrás",
161
+ close="Cerrar",
162
+ last="Finalizar",
163
+ next="Siguiente",
164
+ next_with_progress="Siguiente ({current} de {total})",
165
+ skip="Saltar",
166
+ ),
167
+ )
168
+ ```
169
+
170
+ CSS properties inside `styles` are converted too: `border_radius` becomes
171
+ `borderRadius`.
172
+
173
+ ## Controlled mode
174
+
175
+ Pass `step_index` and react-joyride stops advancing on its own — it renders
176
+ exactly the step your State asks for. Move the index from `on_step_after`:
177
+
178
+ ```python
179
+ class State(rx.State):
180
+ run: bool = False
181
+ index: int = 0
182
+
183
+ @rx.event
184
+ def advance(self, event: rjr.JoyrideEvent):
185
+ self.index = max(0, event["index"] + (-1 if event["action"] == rjr.ACTIONS.PREV else 1))
186
+
187
+ @rx.event
188
+ def skip_missing(self, event: rjr.JoyrideEvent):
189
+ self.index = event["index"] + 1
190
+
191
+ @rx.event
192
+ def finished(self, _event: rjr.JoyrideEvent):
193
+ self.run, self.index = False, 0
194
+
195
+
196
+ rjr.joyride(
197
+ steps=STEPS,
198
+ run=State.run,
199
+ step_index=State.index,
200
+ continuous=True,
201
+ on_step_after=State.advance,
202
+ on_target_not_found=State.skip_missing,
203
+ on_tour_end=State.finished,
204
+ )
205
+ ```
206
+
207
+ This is what you want when a step should only open after your own code did
208
+ something — finished a request, opened a drawer, switched a tab.
209
+
210
+ ## Events
211
+
212
+ Every trigger receives the same flat dict:
213
+
214
+ | key | meaning |
215
+ | --- | --- |
216
+ | `type` | the event, e.g. `tour:start`, `step:after`, `error:target_not_found` |
217
+ | `action` | what triggered it: `start`, `next`, `prev`, `close`, `skip`, `update`, ... |
218
+ | `index`, `size`, `is_last_step` | where we are in the tour |
219
+ | `status` | `idle`, `ready`, `waiting`, `running`, `paused`, `skipped`, `finished` |
220
+ | `lifecycle` | `init`, `ready`, `beacon`, `tooltip`, `complete`, ... |
221
+ | `origin` | which control fired it: `button_primary`, `overlay`, `keyboard`, ... |
222
+ | `controlled`, `waiting`, `scrolling` | tour flags |
223
+ | `step_id`, `step_target`, `step_title`, `step_data` | about the current step |
224
+ | `error` | the message for `error` events |
225
+
226
+ Triggers: `on_event` (all of them), `on_tour_start`, `on_tour_end`,
227
+ `on_status_change`, `on_step_before`, `on_step_after`, `on_beacon`,
228
+ `on_tooltip`, `on_scroll_start`, `on_scroll_end`, `on_target_not_found`,
229
+ `on_error`.
230
+
231
+ Compare against the constants instead of raw strings:
232
+
233
+ ```python
234
+ @rx.event
235
+ def on_tour_event(self, event: rjr.JoyrideEvent):
236
+ if event["type"] == rjr.EVENTS.TOUR_END and event["status"] in rjr.FINISHED_STATUSES:
237
+ self.onboarding_done = True
238
+ ```
239
+
240
+ ## Python actions
241
+
242
+ Give the tour a `tour_id` and you can drive it from anywhere, without keeping
243
+ the index in your State:
244
+
245
+ ```python
246
+ rx.button("Start", on_click=rjr.start("welcome"))
247
+ rx.button("Step 3", on_click=rjr.go_to("welcome", 2))
248
+ rx.button("Next", on_click=rjr.next_step("welcome"))
249
+ rx.button("Replay this step", on_click=rjr.replay("welcome"))
250
+ rx.button("Skip", on_click=rjr.skip("welcome"))
251
+ ```
252
+
253
+ `start`, `stop`, `next_step`, `prev_step`, `go_to`, `close`, `skip`, `reset`,
254
+ `replay`, `open_tooltip`, and `get_state(tour_id, callback)` which sends the
255
+ current tour state to an event handler. They are no-ops (with a console warning)
256
+ when no tour with that id is mounted.
257
+
258
+ The tour still needs `run=True` to be running; the actions drive a running tour,
259
+ they do not switch it on.
260
+
261
+ ## Custom tooltip and beacon
262
+
263
+ Pass a function returning a Reflex component. It receives what react-joyride
264
+ passes to a custom tooltip, plus `current`, `total`, `progress`, `content`,
265
+ `title`, `stepId`, `stepData`, `isFirstStep` and `isLastStep` (these are
266
+ JavaScript props, so their keys stay `camelCase`):
267
+
268
+ ```python
269
+ def tooltip(props):
270
+ return rx.card(
271
+ rx.vstack(
272
+ rx.badge(props["progress"], radius="full"),
273
+ rx.heading(props["title"], size="4"),
274
+ rx.text(props["content"], size="2"),
275
+ rx.hstack(
276
+ rx.button(
277
+ "Back",
278
+ variant="soft",
279
+ on_click=rjr.prev_step("welcome"),
280
+ disabled=props["isFirstStep"].to(bool),
281
+ ),
282
+ rx.button(
283
+ rx.cond(props["isLastStep"].to(bool), "Done", "Next"), on_click=rjr.next_step("welcome")
284
+ ),
285
+ ),
286
+ ),
287
+ width="360px",
288
+ )
289
+
290
+
291
+ rjr.joyride(steps=STEPS, tour_id="welcome", tooltip=tooltip, beacon=lambda _p: rx.box(...))
292
+ ```
293
+
294
+ The component is rendered inside react-joyride's own wrapper, so the ARIA
295
+ attributes and positioning are handled for you. It can read your State like any
296
+ other component.
297
+
298
+ ## Rendering above the overlay
299
+
300
+ react-joyride portals the overlay into `document.body`, and Reflex's Radix theme
301
+ root is a stacking context (`z-index: 0`), so the overlay paints above the whole
302
+ app no matter how large a `z-index` you give your own elements. If you need
303
+ something clickable while a tour runs — a control bar, a chat widget — render
304
+ the tour inside the app instead and give your element a higher `z-index` than
305
+ the tour's:
306
+
307
+ ```python
308
+ (rx.box(id="tour-root"),)
309
+ (rjr.joyride(steps=STEPS, portal_element="#tour-root", options=rjr.TourOptions(z_index=1000)),)
310
+ (rx.box(my_controls, position="fixed", bottom="1rem", left="1rem", z_index="1200"),)
311
+ ```
312
+
313
+ Otherwise anything under the overlay is inert while a step is open, which is
314
+ usually exactly what you want.
315
+
316
+ ## Demo app
317
+
318
+ The `react_joyride_demo/` folder is a four page Reflex app covering the basics,
319
+ controlled mode, theming with a Reflex tooltip, and the awkward cases (targets
320
+ that appear late or never, spotlight targets, scrolling, modal steps, blocked
321
+ interaction):
322
+
323
+ ```bash
324
+ uv sync
325
+ cd react_joyride_demo
326
+ uv run reflex run
327
+ ```
328
+
329
+ ## Development
330
+
331
+ ```bash
332
+ uv sync # creates .venv and installs the package in editable mode
333
+ uv run pytest
334
+ uv run ruff check .
335
+ uv run ruff format --check .
336
+ uv run reflex component build # generates the .pyi stubs and builds dist/
337
+ ```
338
+
339
+ ## License
340
+
341
+ MIT © Ernesto Crespo
@@ -0,0 +1,313 @@
1
+ # reflex-react-joyride
2
+
3
+ Guided product tours and onboarding walkthroughs for [Reflex](https://reflex.dev), powered by [react-joyride](https://react-joyride.com) v3.
4
+
5
+ - Wraps **react-joyride 3.2.0** through its `useJoyride` hook, so the tour's controls are reachable from Python
6
+ - Typed props for everything: `Step`, `TourOptions`, `Locale`, `TourStyles`, `FloatingOptions` — `snake_case` in Python, `camelCase` in the browser
7
+ - **Uncontrolled** (`run`) or **controlled** (`step_index`) mode, whichever fits your State
8
+ - **Python actions**: `start`, `next_step`, `prev_step`, `go_to`, `skip`, `reset`, `replay`, `stop`, `open_tooltip`, `get_state`
9
+ - One catch-all `on_event` plus a dedicated trigger per event (`on_tour_start`, `on_step_after`, `on_target_not_found`, ...) with a flat, JSON-safe payload
10
+ - **Custom tooltip and beacon** written as ordinary Reflex components
11
+ - Step content and titles accept Reflex components, not just strings
12
+ - Constants mirroring react-joyride's literals: `EVENTS`, `ACTIONS`, `STATUS`, `LIFECYCLE`, `ORIGIN`
13
+
14
+ ## Screenshots
15
+
16
+ The four pages of the demo app, in order:
17
+
18
+ | | |
19
+ | --- | --- |
20
+ | ![Basic tour](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/1-tour-basico.png) | ![Controlled mode](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/2-modo-controlado.png) |
21
+ | A continuous tour over a small dashboard, with the event log filling up | Controlled mode: the step index lives in the State and external buttons drive it |
22
+ | ![Spanish locale and theming](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/3-tema-locale-es.png) | ![Custom Reflex tooltip](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/4-tooltip-reflex.png) |
23
+ | `locale`, `options` and `styles` restyling the built-in tooltip | The same step rendered by a Reflex component through the `tooltip` render prop |
24
+ | ![Modal step](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/5-paso-modal.png) | ![Spotlight target](https://raw.githubusercontent.com/ecrespo/reflex-react-joyride/main/docs/screenshots/6-spotlight-y-targets.png) |
25
+ | `placement="center"` on `body` for a modal-like opening step | A spotlight over a different element than the one the tooltip points at |
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install reflex-react-joyride
31
+ # or
32
+ uv add reflex-react-joyride
33
+ ```
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ import reflex as rx
39
+ import reflex_react_joyride as rjr
40
+
41
+
42
+ class TourState(rx.State):
43
+ run: bool = False
44
+
45
+ @rx.event
46
+ def start(self):
47
+ self.run = True
48
+
49
+ @rx.event
50
+ def finished(self, event: rjr.JoyrideEvent):
51
+ self.run = False
52
+
53
+
54
+ def index() -> rx.Component:
55
+ return rx.vstack(
56
+ rjr.joyride(
57
+ tour_id="welcome",
58
+ run=TourState.run,
59
+ continuous=True,
60
+ steps=[
61
+ rjr.Step(target="#logo", title="Welcome", content="This is the app."),
62
+ rjr.Step(target="#cta", content="Start here.", placement="top"),
63
+ ],
64
+ options=rjr.TourOptions(show_progress=True, primary_color="#7c3aed"),
65
+ on_tour_end=TourState.finished,
66
+ ),
67
+ rx.heading("Acme", id="logo"),
68
+ rx.button("Get started", id="cta"),
69
+ rx.button("Take the tour", on_click=TourState.start),
70
+ )
71
+ ```
72
+
73
+ ## Steps
74
+
75
+ A step needs a `target` (any CSS selector) and some `content`. Everything in
76
+ `TourOptions` can be overridden per step:
77
+
78
+ ```python
79
+ rjr.Step(
80
+ target="#sidebar",
81
+ title="Navigation",
82
+ content=rx.vstack(rx.text("Every section lives here."), rx.code("Cmd+K")),
83
+ placement="right", # top | bottom | left | right | auto | center (+ -start / -end)
84
+ spotlight_padding=16,
85
+ show_progress=True,
86
+ id="sidebar", # echoed back as event["step_id"]
87
+ data={"section": "nav"}, # echoed back as event["step_data"], keys untouched
88
+ )
89
+ ```
90
+
91
+ `target="body"` with `placement="center"` gives a modal-like step, which is a
92
+ good opener. Plain dicts work as well as `Step` objects, and so do steps kept in
93
+ a State var:
94
+
95
+ ```python
96
+ class State(rx.State):
97
+ steps: list[dict] = [{"target": "#logo", "content": "Hi", "show_progress": True}]
98
+
99
+
100
+ rjr.joyride(steps=State.steps)
101
+ ```
102
+
103
+ `snake_case` keys are converted to the `camelCase` react-joyride expects, both
104
+ for literal props (in Python) and for values coming from the State (in the
105
+ browser), so you never have to write `spotlightPadding` yourself. Keys that are
106
+ already `camelCase` pass through untouched.
107
+
108
+ ## Theming
109
+
110
+ `options` sets behaviour and colors for the whole tour, `styles` overrides the
111
+ CSS of every element react-joyride renders, and `locale` replaces the strings:
112
+
113
+ ```python
114
+ rjr.joyride(
115
+ steps=STEPS,
116
+ options=rjr.TourOptions(
117
+ primary_color="#059669",
118
+ background_color="#f2fbf7",
119
+ overlay_color="rgba(4, 47, 36, 0.55)",
120
+ show_progress=True,
121
+ skip_beacon=True, # go straight to the tooltip, no beacon
122
+ spotlight_radius=12,
123
+ buttons=["back", "skip", "primary"],
124
+ width=420,
125
+ z_index=1000,
126
+ ),
127
+ styles=rjr.TourStyles(
128
+ tooltip={"border_radius": "14px", "box_shadow": "0 18px 40px rgba(15, 23, 42, 0.18)"},
129
+ button_primary={"border_radius": "999px", "padding": "8px 18px"},
130
+ ),
131
+ locale=rjr.Locale(
132
+ back="Atrás",
133
+ close="Cerrar",
134
+ last="Finalizar",
135
+ next="Siguiente",
136
+ next_with_progress="Siguiente ({current} de {total})",
137
+ skip="Saltar",
138
+ ),
139
+ )
140
+ ```
141
+
142
+ CSS properties inside `styles` are converted too: `border_radius` becomes
143
+ `borderRadius`.
144
+
145
+ ## Controlled mode
146
+
147
+ Pass `step_index` and react-joyride stops advancing on its own — it renders
148
+ exactly the step your State asks for. Move the index from `on_step_after`:
149
+
150
+ ```python
151
+ class State(rx.State):
152
+ run: bool = False
153
+ index: int = 0
154
+
155
+ @rx.event
156
+ def advance(self, event: rjr.JoyrideEvent):
157
+ self.index = max(0, event["index"] + (-1 if event["action"] == rjr.ACTIONS.PREV else 1))
158
+
159
+ @rx.event
160
+ def skip_missing(self, event: rjr.JoyrideEvent):
161
+ self.index = event["index"] + 1
162
+
163
+ @rx.event
164
+ def finished(self, _event: rjr.JoyrideEvent):
165
+ self.run, self.index = False, 0
166
+
167
+
168
+ rjr.joyride(
169
+ steps=STEPS,
170
+ run=State.run,
171
+ step_index=State.index,
172
+ continuous=True,
173
+ on_step_after=State.advance,
174
+ on_target_not_found=State.skip_missing,
175
+ on_tour_end=State.finished,
176
+ )
177
+ ```
178
+
179
+ This is what you want when a step should only open after your own code did
180
+ something — finished a request, opened a drawer, switched a tab.
181
+
182
+ ## Events
183
+
184
+ Every trigger receives the same flat dict:
185
+
186
+ | key | meaning |
187
+ | --- | --- |
188
+ | `type` | the event, e.g. `tour:start`, `step:after`, `error:target_not_found` |
189
+ | `action` | what triggered it: `start`, `next`, `prev`, `close`, `skip`, `update`, ... |
190
+ | `index`, `size`, `is_last_step` | where we are in the tour |
191
+ | `status` | `idle`, `ready`, `waiting`, `running`, `paused`, `skipped`, `finished` |
192
+ | `lifecycle` | `init`, `ready`, `beacon`, `tooltip`, `complete`, ... |
193
+ | `origin` | which control fired it: `button_primary`, `overlay`, `keyboard`, ... |
194
+ | `controlled`, `waiting`, `scrolling` | tour flags |
195
+ | `step_id`, `step_target`, `step_title`, `step_data` | about the current step |
196
+ | `error` | the message for `error` events |
197
+
198
+ Triggers: `on_event` (all of them), `on_tour_start`, `on_tour_end`,
199
+ `on_status_change`, `on_step_before`, `on_step_after`, `on_beacon`,
200
+ `on_tooltip`, `on_scroll_start`, `on_scroll_end`, `on_target_not_found`,
201
+ `on_error`.
202
+
203
+ Compare against the constants instead of raw strings:
204
+
205
+ ```python
206
+ @rx.event
207
+ def on_tour_event(self, event: rjr.JoyrideEvent):
208
+ if event["type"] == rjr.EVENTS.TOUR_END and event["status"] in rjr.FINISHED_STATUSES:
209
+ self.onboarding_done = True
210
+ ```
211
+
212
+ ## Python actions
213
+
214
+ Give the tour a `tour_id` and you can drive it from anywhere, without keeping
215
+ the index in your State:
216
+
217
+ ```python
218
+ rx.button("Start", on_click=rjr.start("welcome"))
219
+ rx.button("Step 3", on_click=rjr.go_to("welcome", 2))
220
+ rx.button("Next", on_click=rjr.next_step("welcome"))
221
+ rx.button("Replay this step", on_click=rjr.replay("welcome"))
222
+ rx.button("Skip", on_click=rjr.skip("welcome"))
223
+ ```
224
+
225
+ `start`, `stop`, `next_step`, `prev_step`, `go_to`, `close`, `skip`, `reset`,
226
+ `replay`, `open_tooltip`, and `get_state(tour_id, callback)` which sends the
227
+ current tour state to an event handler. They are no-ops (with a console warning)
228
+ when no tour with that id is mounted.
229
+
230
+ The tour still needs `run=True` to be running; the actions drive a running tour,
231
+ they do not switch it on.
232
+
233
+ ## Custom tooltip and beacon
234
+
235
+ Pass a function returning a Reflex component. It receives what react-joyride
236
+ passes to a custom tooltip, plus `current`, `total`, `progress`, `content`,
237
+ `title`, `stepId`, `stepData`, `isFirstStep` and `isLastStep` (these are
238
+ JavaScript props, so their keys stay `camelCase`):
239
+
240
+ ```python
241
+ def tooltip(props):
242
+ return rx.card(
243
+ rx.vstack(
244
+ rx.badge(props["progress"], radius="full"),
245
+ rx.heading(props["title"], size="4"),
246
+ rx.text(props["content"], size="2"),
247
+ rx.hstack(
248
+ rx.button(
249
+ "Back",
250
+ variant="soft",
251
+ on_click=rjr.prev_step("welcome"),
252
+ disabled=props["isFirstStep"].to(bool),
253
+ ),
254
+ rx.button(
255
+ rx.cond(props["isLastStep"].to(bool), "Done", "Next"), on_click=rjr.next_step("welcome")
256
+ ),
257
+ ),
258
+ ),
259
+ width="360px",
260
+ )
261
+
262
+
263
+ rjr.joyride(steps=STEPS, tour_id="welcome", tooltip=tooltip, beacon=lambda _p: rx.box(...))
264
+ ```
265
+
266
+ The component is rendered inside react-joyride's own wrapper, so the ARIA
267
+ attributes and positioning are handled for you. It can read your State like any
268
+ other component.
269
+
270
+ ## Rendering above the overlay
271
+
272
+ react-joyride portals the overlay into `document.body`, and Reflex's Radix theme
273
+ root is a stacking context (`z-index: 0`), so the overlay paints above the whole
274
+ app no matter how large a `z-index` you give your own elements. If you need
275
+ something clickable while a tour runs — a control bar, a chat widget — render
276
+ the tour inside the app instead and give your element a higher `z-index` than
277
+ the tour's:
278
+
279
+ ```python
280
+ (rx.box(id="tour-root"),)
281
+ (rjr.joyride(steps=STEPS, portal_element="#tour-root", options=rjr.TourOptions(z_index=1000)),)
282
+ (rx.box(my_controls, position="fixed", bottom="1rem", left="1rem", z_index="1200"),)
283
+ ```
284
+
285
+ Otherwise anything under the overlay is inert while a step is open, which is
286
+ usually exactly what you want.
287
+
288
+ ## Demo app
289
+
290
+ The `react_joyride_demo/` folder is a four page Reflex app covering the basics,
291
+ controlled mode, theming with a Reflex tooltip, and the awkward cases (targets
292
+ that appear late or never, spotlight targets, scrolling, modal steps, blocked
293
+ interaction):
294
+
295
+ ```bash
296
+ uv sync
297
+ cd react_joyride_demo
298
+ uv run reflex run
299
+ ```
300
+
301
+ ## Development
302
+
303
+ ```bash
304
+ uv sync # creates .venv and installs the package in editable mode
305
+ uv run pytest
306
+ uv run ruff check .
307
+ uv run ruff format --check .
308
+ uv run reflex component build # generates the .pyi stubs and builds dist/
309
+ ```
310
+
311
+ ## License
312
+
313
+ MIT © Ernesto Crespo