reflex-react-joyride
Guided product tours and onboarding walkthroughs for Reflex, powered by react-joyride v3.
- Wraps react-joyride 3.2.0 through its
useJoyridehook, so the tour's controls are reachable from Python - Typed props for everything:
Step,TourOptions,Locale,TourStyles,FloatingOptions—snake_casein Python,camelCasein the browser - Uncontrolled (
run) or controlled (step_index) mode, whichever fits your State - Python actions:
start,next_step,prev_step,go_to,skip,reset,replay,stop,open_tooltip,get_state - One catch-all
on_eventplus a dedicated trigger per event (on_tour_start,on_step_after,on_target_not_found, ...) with a flat, JSON-safe payload - Custom tooltip and beacon written as ordinary Reflex components
- Step content and titles accept Reflex components, not just strings
- Constants mirroring react-joyride's literals:
EVENTS,ACTIONS,STATUS,LIFECYCLE,ORIGIN
Screenshots
The four pages of the demo app, in order:
| 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 |
locale, options and styles restyling the built-in tooltip |
The same step rendered by a Reflex component through the tooltip render prop |
placement="center" on body for a modal-like opening step |
A spotlight over a different element than the one the tooltip points at |
Requirements
| Version | |
|---|---|
| Python | 3.11, 3.12 or 3.13 |
| Reflex | 0.10.0 or newer |
| react-joyride | 3.2.0 (bundled, installed by Reflex on first compile) |
| Node.js (development and CI only) | 24 LTS, pinned in .nvmrc |
Upgrading from 0.1.x: 0.2.0 requires Reflex 0.10 and drops Python 3.10, following Reflex itself. The Python API is unchanged. See the changelog.
Installation
pip install reflex-react-joyride
# or
uv add reflex-react-joyride
Quick start
import reflex as rx
import reflex_react_joyride as rjr
class TourState(rx.State):
run: bool = False
@rx.event
def start(self):
self.run = True
@rx.event
def finished(self, event: rjr.JoyrideEvent):
self.run = False
def index() -> rx.Component:
return rx.vstack(
rjr.joyride(
tour_id="welcome",
run=TourState.run,
continuous=True,
steps=[
rjr.Step(target="#logo", title="Welcome", content="This is the app."),
rjr.Step(target="#cta", content="Start here.", placement="top"),
],
options=rjr.TourOptions(show_progress=True, primary_color="#7c3aed"),
on_tour_end=TourState.finished,
),
rx.heading("Acme", id="logo"),
rx.button("Get started", id="cta"),
rx.button("Take the tour", on_click=TourState.start),
)
Steps
A step needs a target (any CSS selector) and some content. Everything in
TourOptions can be overridden per step:
rjr.Step(
target="#sidebar",
title="Navigation",
content=rx.vstack(rx.text("Every section lives here."), rx.code("Cmd+K")),
placement="right", # top | bottom | left | right | auto | center (+ -start / -end)
spotlight_padding=16,
show_progress=True,
id="sidebar", # echoed back as event["step_id"]
data={"section": "nav"}, # echoed back as event["step_data"], keys untouched
)
target="body" with placement="center" gives a modal-like step, which is a
good opener. Plain dicts work as well as Step objects, and so do steps kept in
a State var:
class State(rx.State):
steps: list[dict] = [{"target": "#logo", "content": "Hi", "show_progress": True}]
rjr.joyride(steps=State.steps)
snake_case keys are converted to the camelCase react-joyride expects, both
for literal props (in Python) and for values coming from the State (in the
browser), so you never have to write spotlightPadding yourself. Keys that are
already camelCase pass through untouched.
Theming
options sets behaviour and colors for the whole tour, styles overrides the
CSS of every element react-joyride renders, and locale replaces the strings:
rjr.joyride(
steps=STEPS,
options=rjr.TourOptions(
primary_color="#059669",
background_color="#f2fbf7",
overlay_color="rgba(4, 47, 36, 0.55)",
show_progress=True,
skip_beacon=True, # go straight to the tooltip, no beacon
spotlight_radius=12,
buttons=["back", "skip", "primary"],
width=420,
z_index=1000,
),
styles=rjr.TourStyles(
tooltip={"border_radius": "14px", "box_shadow": "0 18px 40px rgba(15, 23, 42, 0.18)"},
button_primary={"border_radius": "999px", "padding": "8px 18px"},
),
locale=rjr.Locale(
back="Atrás",
close="Cerrar",
last="Finalizar",
next="Siguiente",
next_with_progress="Siguiente ({current} de {total})",
skip="Saltar",
),
)
CSS properties inside styles are converted too: border_radius becomes
borderRadius.
Controlled mode
Pass step_index and react-joyride stops advancing on its own — it renders
exactly the step your State asks for. Move the index from on_step_after:
class State(rx.State):
run: bool = False
index: int = 0
@rx.event
def advance(self, event: rjr.JoyrideEvent):
self.index = max(0, event["index"] + (-1 if event["action"] == rjr.ACTIONS.PREV else 1))
@rx.event
def skip_missing(self, event: rjr.JoyrideEvent):
self.index = event["index"] + 1
@rx.event
def finished(self, _event: rjr.JoyrideEvent):
self.run, self.index = False, 0
rjr.joyride(
steps=STEPS,
run=State.run,
step_index=State.index,
continuous=True,
on_step_after=State.advance,
on_target_not_found=State.skip_missing,
on_tour_end=State.finished,
)
This is what you want when a step should only open after your own code did something — finished a request, opened a drawer, switched a tab.
Events
Every trigger receives the same flat dict:
| key | meaning |
|---|---|
type |
the event, e.g. tour:start, step:after, error:target_not_found |
action |
what triggered it: start, next, prev, close, skip, update, ... |
index, size, is_last_step |
where we are in the tour |
status |
idle, ready, waiting, running, paused, skipped, finished |
lifecycle |
init, ready, beacon, tooltip, complete, ... |
origin |
which control fired it: button_primary, overlay, keyboard, ... |
controlled, waiting, scrolling |
tour flags |
step_id, step_target, step_title, step_data |
about the current step |
error |
the message for error events |
Triggers: on_event (all of them), on_tour_start, on_tour_end,
on_status_change, on_step_before, on_step_after, on_beacon,
on_tooltip, on_scroll_start, on_scroll_end, on_target_not_found,
on_error.
Compare against the constants instead of raw strings:
@rx.event
def on_tour_event(self, event: rjr.JoyrideEvent):
if event["type"] == rjr.EVENTS.TOUR_END and event["status"] in rjr.FINISHED_STATUSES:
self.onboarding_done = True
Python actions
Give the tour a tour_id and you can drive it from anywhere, without keeping
the index in your State:
rx.button("Start", on_click=rjr.start("welcome"))
rx.button("Step 3", on_click=rjr.go_to("welcome", 2))
rx.button("Next", on_click=rjr.next_step("welcome"))
rx.button("Replay this step", on_click=rjr.replay("welcome"))
rx.button("Skip", on_click=rjr.skip("welcome"))
start, stop, next_step, prev_step, go_to, close, skip, reset,
replay, open_tooltip, and get_state(tour_id, callback) which sends the
current tour state to an event handler. They are no-ops (with a console warning)
when no tour with that id is mounted.
The tour still needs run=True to be running; the actions drive a running tour,
they do not switch it on.
Custom tooltip and beacon
Pass a function returning a Reflex component. It receives what react-joyride
passes to a custom tooltip, plus current, total, progress, content,
title, stepId, stepData, isFirstStep and isLastStep (these are
JavaScript props, so their keys stay camelCase):
def tooltip(props):
return rx.card(
rx.vstack(
rx.badge(props["progress"], radius="full"),
rx.heading(props["title"], size="4"),
rx.text(props["content"], size="2"),
rx.hstack(
rx.button(
"Back",
variant="soft",
on_click=rjr.prev_step("welcome"),
disabled=props["isFirstStep"].to(bool),
),
rx.button(
rx.cond(props["isLastStep"].to(bool), "Done", "Next"), on_click=rjr.next_step("welcome")
),
),
),
width="360px",
)
rjr.joyride(steps=STEPS, tour_id="welcome", tooltip=tooltip, beacon=lambda _p: rx.box(...))
The component is rendered inside react-joyride's own wrapper, so the ARIA attributes and positioning are handled for you. It can read your State like any other component.
Rendering above the overlay
react-joyride portals the overlay into document.body, and Reflex's Radix theme
root is a stacking context (z-index: 0), so the overlay paints above the whole
app no matter how large a z-index you give your own elements. If you need
something clickable while a tour runs — a control bar, a chat widget — render
the tour inside the app instead and give your element a higher z-index than
the tour's:
(rx.box(id="tour-root"),)
(rjr.joyride(steps=STEPS, portal_element="#tour-root", options=rjr.TourOptions(z_index=1000)),)
(rx.box(my_controls, position="fixed", bottom="1rem", left="1rem", z_index="1200"),)
Otherwise anything under the overlay is inert while a step is open, which is usually exactly what you want.
Demo app
The react_joyride_demo/ folder is a four page Reflex app covering the basics,
controlled mode, theming with a Reflex tooltip, and the awkward cases (targets
that appear late or never, spotlight targets, scrolling, modal steps, blocked
interaction):
uv sync
cd react_joyride_demo
uv run reflex run
Development
uv sync # creates .venv and installs the package in editable mode
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv build # builds the sdist and wheel into dist/
reflex component was removed in Reflex 0.10. The .pyi stubs are committed;
regenerate them after changing the component's props:
uv run python -m reflex_base.utils.pyi_generator custom_components/reflex_react_joyride
CI runs on Node 24 (.nvmrc); use the same version locally (nvm use) if you
compile the demo with Node instead of Bun.
Releasing
Releases are published to PyPI by the Release workflow through Trusted
Publishing, so no token is stored anywhere:
-
On
develop, bumpversioninpyproject.tomland add an entry toCHANGELOG.md. -
Open a PR from
developtomainand merge it once the Quality and Security workflows pass. -
Tag the merge commit on
mainand push the tag:git switch main && git pull git tag v0.2.0 && git push origin v0.2.0
The workflow refuses tags that are not on main or that don't match the
pyproject.toml version, then runs the tests, builds, publishes to PyPI with
build provenance attestations and creates the GitHub release.
License
MIT © Ernesto Crespo
Metadata
Release files for reflex-react-joyride 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| reflex_react_joyride-0.2.0.tar.gz | 29.2 kB | With provenance | Details for reflex_react_joyride-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| reflex_react_joyride-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for reflex_react_joyride-0.2.0-py3-none-any.whl |
Total release size: 55.2 kB
Release files / reflex_react_joyride-0.2.0.tar.gz
| Download URL | reflex_react_joyride-0.2.0.tar.gz |
|---|---|
| Size | 29.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3aee4523ae8ae741e0282e0aad11452915f4428389ae360955f66e5c60679ada
|
|
BLAKE2b-256 checksum How to use checksums |
af12d6abba727684e9e09c81b49afc86b49ded4f631b641814e9e11acf9811ee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / reflex_react_joyride-0.2.0-py3-none-any.whl
| Download URL | reflex_react_joyride-0.2.0-py3-none-any.whl |
|---|---|
| Size | 26.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
da9d3e9b47debe471a78475d1eb6a908a7c2bbe679b989a0cf5d1a89679d9c4c
|
|
BLAKE2b-256 checksum How to use checksums |
8ba45fd67f65d65f95f1a8affd4f073af29debd9d208fa751aeabbb2e54b339d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for reflex-react-joyride 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for reflex_react_joyride-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.