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, FloatingOptionssnake_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

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 run reflex component build   # generates the .pyi stubs and builds dist/

License

MIT © Ernesto Crespo

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

reflex_react_joyride-0.1.0.tar.gz (27.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

reflex_react_joyride-0.1.0-py3-none-any.whl (25.3 kB view details)

Uploaded Python 3

File details

Details for the file reflex_react_joyride-0.1.0.tar.gz.

File metadata

  • Download URL: reflex_react_joyride-0.1.0.tar.gz
  • Upload date:
  • Size: 27.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for reflex_react_joyride-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5b4da47dcac91df62d2f4ea7b294f372b2504c41053ba01d158e13bca018d185
MD5 5ec2c1c7116862262cb751112f530fdf
BLAKE2b-256 25af14f33af0cf4684243c08dda461d9909ba47bedc0fe20c0884f1839bed744

See more details on using hashes here.

Provenance

The following attestation bundles were made for reflex_react_joyride-0.1.0.tar.gz:

Publisher: release.yml on ecrespo/reflex-react-joyride

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file reflex_react_joyride-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for reflex_react_joyride-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 43c17a727218dfe53742b01d9f0df612667c939b6c87311824b9b41ed6506cea
MD5 cc08e220c6cd51227e624f526e678b36
BLAKE2b-256 5675527402ba8a86ebd80f8b281a597b2cf9ff6deea776d11d6bc1c2f0acf39a

See more details on using hashes here.

Provenance

The following attestation bundles were made for reflex_react_joyride-0.1.0-py3-none-any.whl:

Publisher: release.yml on ecrespo/reflex-react-joyride

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

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