Skip to main content

reflex-react-joyride

Guided product tours and onboarding walkthroughs for Reflex, powered by react-joyride v3.

  • Wraps react-joyride 3.2.0 through its useJoyride hook, so the tour's controls are reachable from Python
  • Typed props for everything: Step, TourOptions, Locale, TourStyles, FloatingOptions — snake_case in Python, camelCase in 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_event plus 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:

Basic tour Controlled mode
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
Spanish locale and theming Custom Reflex tooltip
locale, options and styles restyling the built-in tooltip The same step rendered by a Reflex component through the tooltip render prop
Modal step Spotlight target
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 to 3.14
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 and later require Reflex 0.10 and drop 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 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 demo
uv run reflex run

Development

The project follows the layout of Reflex's official component template: the package lives in src/reflex_react_joyride/, it builds with hatchling, the version comes from the git tag, and the .pyi stubs are generated at build time by hatch-reflex-pyi instead of being committed.

uv sync                                # creates .venv, installs the package in editable mode
uv run pre-commit install              # once: ruff, pyright and file checks on every commit
uv run pre-commit run --all-files      # lint, format and type-check
uv run pytest
rm -rf dist && uv build                # sdist and wheel, stubs included, into dist/

uv sync writes src/reflex_react_joyride/joyride.pyi the first time (it is git-ignored). The build hook skips generation when the version hasn't changed, so after changing the component's props regenerate the stub yourself, and always build into an empty dist/:

cd src && uv run python -m reflex_base.utils.pyi_generator reflex_react_joyride && rm -rf assets

reflex component init/build no longer exist in Reflex 0.10; the template and the commands above replace them. See AGENTS.md for the conventions coding agents (and humans) should follow.

CI runs on Node 24 (.nvmrc); use the same version locally (nvm use) if you compile the demo with Node instead of Bun.

Releasing

The version is not written anywhere in the code: it comes from the git tag (uv-dynamic-versioning). Releases are published to PyPI by the Release workflow through Trusted Publishing, so no token is stored anywhere:

  1. On develop, add an entry to CHANGELOG.md.

  2. Open a PR from develop to main and merge it once the Quality and Security workflows pass.

  3. Tag the merge commit on main and push the tag:

    git switch main && git pull
    git tag v0.3.0 && git push origin v0.3.0
    

The workflow refuses tags that are not on main, runs the tests, builds, checks that the built version matches the tag and that the stubs are in the wheel, publishes to PyPI with build provenance attestations and creates the GitHub release.

License

MIT © Ernesto Crespo

Metadata

Release files for reflex-react-joyride 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for reflex-react-joyride 0.3.0
File Size Uploaded Provenance Actions
reflex_react_joyride-0.3.0.tar.gz 74.1 kB With provenance Details for reflex_react_joyride-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for reflex-react-joyride 0.3.0
File Interpreter ABI Platform Actions
reflex_react_joyride-0.3.0-py3-none-any.whl Python 3 none any Details for reflex_react_joyride-0.3.0-py3-none-any.whl

Total release size: 100.4 kB

Release files / reflex_react_joyride-0.3.0.tar.gz

Download URL reflex_react_joyride-0.3.0.tar.gz
Size 74.1 kB
Tags Source
SHA-256 checksum
How to use checksums
b3fddf12af32f98eb9cb5bda9df1319f2c5d7628aa2adccc9259db0454e6f86d
BLAKE2b-256 checksum
How to use checksums
b9598881f2a6ecb66ca3240a577907b6b06f2f180cb0d969d8c744769fe60198
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 log

Release files / reflex_react_joyride-0.3.0-py3-none-any.whl

Download URL reflex_react_joyride-0.3.0-py3-none-any.whl
Size 26.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8eb0db893dde8bf512e03223c678ffd4b464815d4f71541551835d87f5ed0e78
BLAKE2b-256 checksum
How to use checksums
8c0605e637f37d16509494347f57c6553de21108f67764f0ab7a8c5c914bde19
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 log

Security for reflex-react-joyride 0.3.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.

Select from 2 files

Attestation for reflex_react_joyride-0.3.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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page