Click. Capture. Ship.
A visual authoring layer on top of react-joyride. Click the real elements in your running app β the skill writes the tour.
Install Β· Why Β· How it works Β· Settings Β· Contributing Β· Roadmap
A note on attribution. This is a tooling layer. The runtime engine, the API, and most of the cleverness belong to Gil Barbara and the react-joyride contributors β react-joyride is itself MIT-licensed open-source software, and Joyride Studio is an independent project, not affiliated with or endorsed by them. If you'd rather hand-write tours, use react-joyride directly. That's also a great answer.
npx skills add parakeet09/joyride-studioThat uses the skills CLI to drop the agent into your .claude/skills/. Restart Claude Code and /joyride-studio init becomes available as a slash command.
Project install: lands in
./.claude/skills/joyride-studio/(committable, shared with team). Global install:npx skills add parakeet09/joyride-studio -glands in~/.claude/skills/.
Hand-writing a Joyride tour means hunting for a stable CSS selector, eyeballing where the tooltip should sit, and crossing your fingers when the DOM shifts.
Joyride Studio replaces that loop with three actions:
![]() |
![]() |
![]() |
| 1. Pick a screen, click an element | 2. Fill in title + body | 3. Run /joyride-studio β done |
The skill takes care of everything else:
| You stop worrying about | Because Joyride Studio handles |
|---|---|
| Picking a stable CSS selector | Walks the React fiber, injects a data-tour-id into the real JSX |
| Tooltip looking out of place | Regenerates TourTooltip and TourButton using your repo's design tokens at init |
| Tooltip drifting from its target | Forwards floatingOptions.autoUpdate + an opt-in MutationObserver watcher |
| Tours looking broken on mobile | Viewport-aware overrides: larger beacon, modal placement, sticky-header offset |
| Wiring multiple tours per route | Built-in registry with shouldShow predicates + useTourRefreshOnMount |
| Replay trigger UI | Auto-mounted ? button with a per-route menu, or roll your own via useAvailableTours() |
What Joyride Studio doesn't do: change anything about how react-joyride itself works at runtime. Every option react-joyride ships is reachable from the Settings panel; nothing is hidden.
Heads-up: GitHub auto-generates an outline for this README. Use the outline button in the top-right of GitHub's file viewer for full navigation β the hero links above cover the headline sections.
Running /joyride-studio init in any React + Vite repo installs and wires up:
| Piece | What it does |
|---|---|
Tour runtime (src/tour/) |
React provider, ? replay button, custom Joyride tooltip themed to your repo's design system, route β tour registry |
Capture inspector (src/components/dev/) |
Floating dev toolbar (ββ§U) β click any element on screen to turn it into a tour step |
Vite dev plugin (vite-tour-plugin.ts) |
REST endpoints the inspector POSTs captures to; persists them as JSON |
Capture storage (.tour-flow/) |
Per-screen JSON files, committed to the repo, versionable |
None of it runs in production β the inspector is gated behind import.meta.env.DEV && VITE_TOUR_AUTHORING === 'true' and stripped by Vite's build.
A developer on a React + Vite app who wants:
- β Guided first-run tours for new users
- β Tooltips that match the app's existing design system (not generic-looking)
- β Multiple independent tours on different screens (and even different states of the same screen)
- β A
?button for returning users to replay the tour on demand - β Zero manual coordinate fiddling β click the real element in the running app, that becomes the tour anchor
Not yet supported in v1: Next.js, CRA, webpack-only setups.
flowchart LR
subgraph once[" Once per repo "]
direction TB
A1[/joyride-studio init/] --> A2[Detect design system<br/>install react-joyride<br/>copy template<br/>regen tooltip + button<br/>wire TourProvider]
end
subgraph repeat[" Repeat per screen "]
direction TB
B1[Cmd-Shift-U toolbar] --> B2[Click element<br/>fill title + body] --> B3[(.tour-flow/<br/>screens/*.json)]
end
subgraph build[" After each batch "]
direction TB
C1[/joyride-studio/] --> C2[Inject data-tour-id<br/>generate step files<br/>update registry<br/>remove inspector mount]
end
once --> repeat --> build --> done([Wired tour, themed to your UI])
style A1 fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a
style B1 fill:#fef3c7,stroke:#f59e0b,color:#78350f
style C1 fill:#fde68a,stroke:#d97706,color:#78350f
style done fill:#dcfce7,stroke:#16a34a,color:#14532d
Under the hood: clicked elements are identified via React's internal _debugStack (React 19) or _debugSource (React 18) β that's how the toolbar can turn a click into a fileName:lineNumber pointing at the real JSX. The build step uses this plus the captured class names to inject a stable data-tour-id attribute into the right component.
The Settings tab exposes every option react-joyride ships, organised the same way as their own playground, plus two Joyride Studio additions on top: Positioning Stability (floating-ui autoUpdate + an opt-in MutationObserver) and Mobile (viewport-aware overrides).
Changes are debounced and PUT to .tour-flow/config.json via the Vite plugin. The file is committable β your config travels with the tour.
- React 18 or 19
- Vite 4 or 5 (dev server; plugin is
apply: 'serve', no prod impact) react-router-dom(the TourProvider callsuseLocation)- Node.js with a package manager detected by the skill (pnpm / yarn / npm)
# In your React + Vite repo (or the right workspace folder if monorepo):
/joyride-studio init
/joyride-studio start
# Set VITE_TOUR_AUTHORING=true in .env.local, then restart dev server.
# Press ββ§U anywhere in your running app to open the Tour Inspector.
# Capture some screens, then:
/joyride-studio
# Done. Reload with cleared localStorage to see the tour.
One-time setup per repo.
What happens, in order:
| Step | What the skill does |
|---|---|
| Framework gate | Reads package.json, confirms Vite + React 18/19, rejects Next.js |
| Design-system detection | Looks for docs/DESIGN_TOKENS.md, COMPONENT_LIBRARY.md; scans src/components/ui/ for primitives; checks for Tailwind / Chakra / MUI / Mantine / Radix |
| Install | Runs pnpm add react-joyride (or yarn/npm based on lockfile) |
| Copy template | Drops runtime + inspector files into src/tour/ and src/components/dev/, plus vite-tour-plugin.ts at the app root |
| Regenerate UI | Rewrites TourTooltip.tsx and TourButton.tsx using YOUR repo's Button/Box/IconButton primitives with correct enum variants and design tokens. If no UI library is found, keeps the raw-HTML + CSS-variable defaults |
| Register plugin | Adds tourPlugin() to vite.config.ts plugins array |
| Wrap app | Inserts <TourProvider> inside your BrowserRouter, adds <TourButton /> |
| Interview | Asks you once: what expression tells your app a user is new? and what identifies the user for completion tracking? Writes both to .tour-flow/config.json and wires them as props |
| Seed | Creates .tour-flow/screens/ and a commented VITE_TOUR_AUTHORING hint in .env.local |
Final report looks like:
β Installed react-joyride
β Copied template files to src/tour/ and src/components/dev/
β Regenerated TourTooltip to use your Box + Button primitives
β Registered tourPlugin in vite.config.ts
β Wrapped app in TourProvider (autoStart=isNewUser, userId=user?.sub)
β Seeded .tour-flow/
β Wrote .env.local hint
Inserts this env-gated block into your app entry (typically main.tsx):
{/* tour-inspector-mount: managed by /joyride-studio skill β do not edit manually */}
{import.meta.env.DEV && import.meta.env.VITE_TOUR_AUTHORING === 'true' && (
<TourStepInspector />
)}
{/* end tour-inspector-mount */}Then tells you to:
- Add
VITE_TOUR_AUTHORING=trueto.env.local - Restart the Vite dev server (Vite reads
.env.localat startup only)
π‘ The inspector is completely inert until both gates are satisfied. Safe to commit the block; it'll never ship to production.
During init the skill asks where tours should be discoverable after a new user's first walkthrough. Pick one:
| Option | What it gives you | When to pick |
|---|---|---|
? bottom-left + menu (default) |
Auto-installed <TourButton />. Hidden when no tours match the current route; otherwise opens a dropdown with one entry per available tour, each with a play button. |
Zero-setup, works on any screen |
| Navbar / header integration | Skill injects a "Guided tours" menu item into your nav using your own primitives (Menu/MenuItem/etc) + the useAvailableTours hook |
You want tours discoverable from a help menu/dropdown |
| Sidebar / settings item | Same as above but in a sidebar or settings panel | Power-user / docs surface |
| Manual | Nothing auto-mounted. You render your own trigger via useAvailableTours() |
You want full control over placement + styling |
For custom placements, useAvailableTours() is the single source of truth:
import { useAvailableTours } from '@/tour';
function HelpMenu() {
const { tours, play } = useAvailableTours();
if (tours.length === 0) return null;
return (
<Menu label="Guided tours">
{tours.map((t) => (
<MenuItem key={t.tourSlug} onClick={() => play(t.tourSlug)}>
{t.name ?? t.tourSlug}
</MenuItem>
))}
</Menu>
);
}tours is an array of every tour route-matching the current path whose shouldShow predicate passes. Each entry has tourSlug, optional name + description, and the full step list. Call play(slug) to run.
The default TourTooltip is fluid β minWidth: 260px, maxWidth: 420px, width: max-content β so short steps stay compact and long ones get breathing room. The footer uses flex-wrap with rowGap, so Back/Skip/Next wrap to a second row on narrow widths instead of overlapping the progress indicator.
A dev-only alignment guard runs after each render. If the tooltip shell or footer's scrollWidth exceeds its clientWidth, it logs a console warning:
[tour] tooltip content overflows its container β buttons or copy may be clipped.
{ step: 3, of: 5, title: "...", shell: {...}, footer: {...}, hint: "..." }
Fix by shortening the step's title/body, or widening the tooltip via Settings β Appearance β width. Never fires in production.
The inspector has two tabs: Capture (screen/step authoring) and Settings. The Settings tab exposes every react-joyride knob organized into the 9 categories that mirror their own playground β plus two joyride-studio additions that sit on top of Joyride:
| Category | What you control |
|---|---|
| Tour Options | continuous, showProgress, showSkip, scrollToFirstStep, spotlightClicks, debug |
| Appearance | primaryColor, backgroundColor, textColor, zIndex, tooltip width |
| Arrow | arrowBase, arrowSize, arrowSpacing, arrowColor |
| Beacon | beaconSize, beaconTrigger (click/hover), skipBeacon |
| Overlay & Spotlight | overlayColor, hideOverlay, spotlightPadding, spotlightRadius, blockTargetInteraction |
| Scroll Behavior | scrollDuration, scrollOffset, skipScroll, disableScrollParentFix |
| Interactions | overlayClickAction, dismissKeyAction, closeButtonAction, disableFocusTrap, targetWaitTimeout, beforeTimeout, loaderDelay |
| Positioning Stability (joyride-studio) | autoUpdate master switch, ancestorScroll / ancestorResize / elementResize / layoutShift / animationFrame axes, observeMutations (MutationObserver on the active target), mutationThrottle |
| Mobile (joyride-studio) | enabled, breakpoint (max-width px), placement, beaconSize, spotlightPadding, scrollOffset, width, isFixed, skipBeacon, disableScroll |
| Custom Components | Paths to tooltipComponent / beaconComponent / arrowComponent / loaderComponent overrides |
| Locale | Button labels: back, close, last, next, nextWithProgress, open, skip |
Changes are debounced and PUT to .tour-flow/config.json via the vite plugin. The file is committable β config travels with the tour.
Tooltips that drift away from their anchor element during an animation or layout shift are the single most common authoring bug. react-joyride uses @floating-ui/react-dom under the hood, and we forward floating-ui's autoUpdate so you get native handling of:
- Scroll on any ancestor (sticky toolbars, scrollable modals, iframesβ¦)
- Viewport or ancestor resize
- The target element itself resizing
- Browser-reported layout shifts
Enable animationFrame only for targets that animate continuously β it runs every frame and is expensive. For class/attribute changes that don't register as a size change (e.g. a CSS transform finishing), flip on observeMutations: joyride-studio adds a MutationObserver to the active step's target and dispatches a throttled reposition when mutations fire.
When the viewport is narrower than breakpoint (default 768px) the provider swaps in mobile-friendly values for the running tour. Desktop tours are untouched β the overrides flip back automatically when you resize.
Sensible defaults:
| Field | Default | Why |
|---|---|---|
placement |
center |
Full-screen modal-style steps avoid positioning problems on phones |
beaconSize |
44 |
Apple's minimum recommended touch-target size |
spotlightPadding |
6 |
Tighter focus ring fits small screens better |
scrollOffset |
64 |
Room for a sticky mobile header |
width |
92vw |
Tooltip nearly fills the viewport |
skipBeacon |
true |
Tap-anywhere-to-advance is more discoverable on touch |
disableScroll |
false |
Flip on if scrolling to the target dismisses the virtual keyboard |
Navigate to the screen you want to build a tour for, then:
- Press ββ§U (Ctrl+Shift+U on Windows/Linux)
- A dark toolbar appears top-right of the viewport
- Click "Pick / create a screen"
- First time: fill a screen ID (e.g.
landing.hero) and a description - Returning: pick from existing screens
- First time: fill a screen ID (e.g.
- Click "+ Add step"
- Toolbar button turns orange:
β Click an elementβ¦ - Move your mouse over the app; elements get a blue outline as you hover
- Toolbar button turns orange:
- Click the element you want the tour step to anchor to
- A form popup appears with the step's fields
- Fill the form, click "Save step"
- Step lands in
.tour-flow/screens/<screenId>.json - Now listed in the toolbar; you can edit, reorder with ββ, or delete
- Step lands in
- Repeat for as many steps as you want on this screen
- "Switch screen" to capture a different screen (e.g.
editor.defaulton/presentation/*)
β‘ The inspector never edits your source files during capture β it only records metadata (file path, line number, classes, tag, selector, viewport). The actual source edits happen in the build step, where you can review all changes atomically.
After you've captured your screens:
| Phase | What happens |
|---|---|
| Load context | Reads all .tour-flow/screens/*.json, your config, design docs |
| Interview (first run only) | 6 global config questions (continuous?, progress?, skip?, overlay click?, ESC?, z-index) β writes .tour-flow/config.json |
| Per-screen routing | For each screen, infers route pattern from the captured URL. When description implies a sub-state condition (e.g. "unauthenticated", "idle state"), asks for a shouldShow predicate |
| Per-step target resolution | Class-match first (using the captured classes string) inside the captured file. Falls back to line+5 lookahead. Reports strategy used per step |
Inject data-tour-id |
Adds data-tour-id="<screen>.<n>" to the JSX opening tag. Idempotent β skip if already present with same value; ask before overwriting with a different value |
| Generate step files | Writes src/tour/steps/<screenId>.tsx β a typed Joyride Step[] using your injected IDs and the captured titles/bodies/media/behavior flags |
| Update registry | Regenerates the TOUR_REGISTRY array in src/tour/registry.ts with entries in correct precedence order |
| Remove inspector mount | Deletes the env-gated block from your entry file (infra files stay, ready for next capture session) |
| Verify | agent-browser navigates to each tour's route, screenshots each step to docs/screenshots/joyride-studio/, flags error:target_not_found console messages |
// In devtools console on the target route:
localStorage.clear()
location.reload()If autoStart evaluates truthy, the tour auto-plays. Otherwise hit the ? button bottom-left to replay.
A finished tour, played in a real app, looks like this β note the same Back/Skip/Next progression on every step, themed to your design tokens:
![]() |
![]() |
![]() |
| Step 1 β first step, Skip + Next | Step 2 β Back appears once you advance | Step 3 β Done on the final step |
| Command | Purpose | When to run |
|---|---|---|
/joyride-studio init |
One-time scaffold + design-system-matching UI generation | Once per repo |
/joyride-studio start |
Insert the <TourStepInspector /> mount in your entry file |
When you want to (re-)enter capture mode |
/joyride-studio |
Builds the tour from captures β generate step files + inject IDs + update registry + remove mount | After each batch of captures |
/joyride-studio verify |
agent-browser pass over existing tours; screenshots + anchor checks | After code refactors, in CI |
/joyride-studio clear <screenId> |
Remove one tour's wiring (step file + registry entry). Keeps captures + data-tour-id attrs |
When retiring a tour |
/joyride-studio teardown |
Remove all skill-installed infra (runtime, inspector, plugin, dep). Keeps .tour-flow/ captures |
Stopping use of the agent |
Each captured screen has:
| Field | Example | Used for |
|---|---|---|
screenId |
landing.hero |
File name; tour slug in registry; localStorage completion key |
route |
/ or /presentation?id=abc |
Route pattern inference (query string stripped) |
description |
"Landing hero β unauthenticated visitor" | Feeds Phase 3.2 to decide if shouldShow predicate is needed |
π’ Tip: Put conditional wording in the description (
unauthenticated,idle state,post-generation). The build step uses it to ask you the right gate question.
Visible in the capture form popup:
| Field | Purpose |
|---|---|
| Title | Tooltip heading (required) |
| Body | Tooltip paragraph. Markdown not supported; plain text only |
| Placement | Tooltip position vs target: auto, top, bottom, left, right, center, + -start/-end variants |
| Media | text / image / video / iframe β adds a rich-media block below the body |
| Media URL | For non-text media |
| Alt text | For images (accessibility) |
Under the "Behavior flags" disclosure in the form. Each maps to a Joyride v3 Step field at build time.
| Flag | When to use |
|---|---|
requireInteraction |
User must interact with target to advance (hides Next button, listens for target click) |
hideClose |
Remove the Γ from the tooltip header |
hideBack |
Remove the Back button |
hideFooter |
Completely hide the button row (use with requireInteraction) |
skipBeacon |
Skip the pulsing dot β go straight to tooltip |
skipScroll |
Don't auto-scroll to target (default is ON so off-screen anchors scroll into view) |
isFixed |
Pin tooltip to viewport for sticky/fixed-position targets |
hideOverlay |
No backdrop dim for this step |
blockTargetInteraction |
Spotlight blocks clicks on the target through it |
spotlightPadding (number) |
Pixels around cutout |
spotlightRadius (number) |
Border-radius of cutout |
targetWaitTimeout (number) |
How long Joyride waits for a lazy-rendered target before failing |
| Kind | Renders |
|---|---|
text |
Just the body β no media block |
image |
<img src={url} alt={alt}> inside a rounded-border wrapper |
video |
<video autoPlay loop muted playsInline src={url}> β good for short looping demos |
iframe |
16:9 wrapper for YouTube / Loom / any embed |
Multiple tours can share a route pattern β e.g. generator.idle and generator.outline-ready both at /. The build step adds a shouldShow predicate to each so they're mutually exclusive:
{
tourSlug: 'generator.idle',
route: '/',
shouldShow: () =>
typeof document !== 'undefined' &&
!!document.querySelector('[data-tour-id="generator.idle.1"]'),
steps: generatorIdleSteps,
},
{
tourSlug: 'generator.outline-ready',
route: '/',
shouldShow: () =>
typeof document !== 'undefined' &&
!!document.querySelector('[data-tour-id="generator.outline-ready.1"]'),
steps: generatorOutlineReadySteps,
},getTourForPath iterates all matches and returns the first that passes shouldShow. Anchors are mutually exclusive (only one sub-view is mounted at a time), so the predicates disambiguate.
Any tour can have a shouldShow: () => boolean predicate. When it returns false:
TourButtonrenders null on this routeautoStartis suppresseduseTour().touris null
Common predicates:
// DOM anchor probe (auto-generated for multi-tour-per-route cases)
shouldShow: () => !!document.querySelector('[data-tour-id="<slug>.1"]')
// Auth state
shouldShow: () => window.__auth?.isAuthenticated === true
// App state flag
shouldShow: () => window.__genState === 'idle'When two tours share a route and differ only by sub-state, the TourProvider needs to know when that sub-state changes. The build step patches each view component that owns a tour's anchors with:
import { useTourRefreshOnMount } from '@/tour';
export function OutlineEditor() {
useTourRefreshOnMount();
// ...
}This triggers a registry re-lookup whenever the view mounts β which is exactly when the sub-state transitions on the same URL.
π Under the hood, the hook calls the provider's
refresh()function. The provider also listens for a window'tour:refresh'event, so you can trigger the same re-lookup from anywhere viawindow.dispatchEvent(new Event('tour:refresh')).
The TourProvider accepts an autoStart: boolean prop. When true AND the current route's tour hasn't been completed, the tour auto-plays on mount.
During init, the skill asks you for an expression:
// Simple: hardcoded
<TourProvider autoStart={false}>
// From auth context
<TourProvider autoStart={isNewUser}>
// From localStorage
<TourProvider autoStart={!localStorage.getItem('tour.everCompleted')}>
// Composite
<TourProvider autoStart={userSynced && syncedUser?.is_new_user === true}>
β οΈ If the expression references identifiers likeisNewUserthat aren't imported yet, the skill prints a reminder. You're responsible for making them resolve.
Same pattern. Used only for the localStorage completion key namespace:
tour.completed.<userId>.<tourSlug> = '1'
Pass null for anonymous sessions β the tour still runs, but completion won't persist across reloads.
If your new-user flag is set asynchronously (e.g. after a /users/sync call resolves), fire tour:refresh so the provider re-resolves:
useEffect(() => {
if (userSynced && user) {
window.__isNewUser = !!syncedUser.is_new_user;
window.requestAnimationFrame(() => {
window.dispatchEvent(new Event('tour:refresh'));
});
}
}, [userSynced, user]);The provider picks this up automatically β no listener setup required.
Zero-copy: the skill is self-contained. Copy the entire .claude/skills/joyride-studio/ directory (with SKILL.md and template/) into any repo's .claude/skills/ directory, then run /joyride-studio init from Claude Code.
# From your new repo's root:
cp -r /path/to/source/.claude/skills/joyride-studio .claude/skills/Then in Claude Code for that repo:
/joyride-studio init
The skill:
- Detects that repo's framework, design system, and UI primitives
- Generates a
TourTooltipusing that repo's components (or falls back to raw HTML defaults) - Seeds
.tour-flow/, wiresTourProvider, installsreact-joyride - Asks for that repo's
autoStartanduserIdexpressions
Nothing is hard-coded to a specific repo.
π¦ If you want to package the skill for distribution (npm, git submodule, script installer), the
template/directory is the canonical source of the files that get copied. Build your installer around that.
- Check env var: in devtools console β
import.meta.env.VITE_TOUR_AUTHORINGshould be"true". If not, edit.env.localand restart the dev server (Vite reads env at startup only). - Check the mount:
document.querySelector('[data-tour-inspector-root]')should return a hidden span element. If null, run/joyride-studio startagain. - Shortcut collision: ββ§T was avoided because it reopens closed tabs in most browsers. If ββ§U is also taken in your setup, edit the keydown check in
src/components/dev/TourStepInspector.tsx(one line).
Tailwind v4 preflight zeroes appearance on inputs. The inspector explicitly sets appearance: 'auto' + colorScheme: 'dark' + fixed dimensions to override this β confirm this block is present in your copy of TourStepInspector.tsx. If your app has a global CSS reset applied after inline styles, you may need to nudge the specificity further.
The data-tour-id was injected but the component's wrapper doesn't forward the data-* attr to the DOM. Common with hand-rolled components that only spread whitelisted props. Options:
- Switch to an ancestor: re-capture the step targeting a parent element that does render a plain DOM node
- Fix the wrapper: make it accept
...restand forward to its underlying DOM element
Your userId expression is returning null or undefined β completion state requires a stable user ID to namespace. Check the expression you gave during init resolves correctly in the runtime scope.
The generated TourTooltip.tsx uses your design system's tokens. If they adapt to dark mode correctly for other components, the tooltip should too. If not:
- Confirm the token classes used (e.g.
bg-card,text-text-default) have dark-mode variants in your tailwind config - For fallback/unthemed repos: edit
src/tour/tokens.tsβ it's driven byvar(--tour-bg),var(--tour-text)CSS variables
Delete the injected attr by hand, then edit the capture in .tour-flow/screens/<screenId>.json and change targetFrameIndex to point at a different stack frame. Re-run /joyride-studio.
Check getTourForPath in src/tour/registry.ts β it should iterate all matches and skip entries whose shouldShow returns false. If your registry has multiple same-route entries but only one triggers, make sure each has a DOM-probing shouldShow pointing at its own first-step anchor.
/joyride-studio teardown
Removes:
src/tour/(runtime)src/components/dev/TourStepInspector.tsx+tourTypes.ts(leavesfiberSource.tsifFeedbackInspectoruses it)vite-tour-plugin.ts- Plugin registration in
vite.config.ts TourProvider+TourButtonwrapping in your entry filereact-joyridefrompackage.json- The env-gated inspector mount if present
Does NOT remove:
.tour-flow/captures (your content β still useful if you reinstall).env.localentries (your env is yours)- Generated step files under
src/tour/steps/(in case you want to keep the current tour as static) data-tour-idattrs injected into your source (manual cleanup if desired)
After init runs:
<your-repo>/
βββ .tour-flow/
β βββ config.json # global Joyride settings
β βββ screens/
β βββ landing.hero.json # captured β committed
β βββ editor.default.json
β
βββ src/
β βββ components/dev/
β β βββ TourStepInspector.tsx # floating toolbar
β β βββ fiberSource.ts # React fiber β source info
β β βββ tourTypes.ts
β βββ tour/
β βββ TourProvider.tsx # React context + Joyride wiring
β βββ TourButton.tsx # ? button (generated to match your UI)
β βββ TourTooltip.tsx # tooltip (generated to match your UI)
β βββ TourMedia.tsx # text/image/video/iframe
β βββ tokens.ts # Joyride styles + locale
β βββ registry.ts # route β tour map
β βββ index.ts # public barrel
β βββ steps/
β βββ landing.hero.tsx # GENERATED after /joyride-studio
β βββ editor.default.tsx
β
βββ vite-tour-plugin.ts # Vite dev plugin
βββ vite.config.ts # patched: imports + registers tourPlugin()
βββ src/main.tsx # patched: <TourProvider> + <TourButton />
βββ .env.local # VITE_TOUR_AUTHORING=true (gitignored)
After /joyride-studio start, main.tsx also contains the env-gated inspector mount. After /joyride-studio, that block is removed.
Joyride Studio is an open-source project. The goal is for every react-joyride capability to be reachable from the visual helper β if you find one that isn't, that's a bug worth filing.
High-value contribution areas:
- Missing Joyride options. If the Settings panel doesn't expose a knob react-joyride ships, add it to
SETTINGS_SCHEMAintemplate/src/components/dev/TourSettingsPanel.tsx, the type intemplate/src/components/dev/tourTypes.ts, and (if it belongs at runtime) the TourProvider wiring. - Framework support. v2 targets Next.js (App Router + Pages Router). v3 aims at CRA / webpack-only setups.
- Richer UI. The default tooltip is framework-neutral by design, but the authoring-side inspector has room for more ergonomic controls (step reorder via drag, media preview, per-step diff against config).
- Mobile polish. Mobile-specific regressions (sticky-header collisions, iOS safe-area inset handling, gesture conflicts) are genuinely hard and always welcome.
- Documentation + recipes. Screenshots, GIFs, and real-world examples for tricky cases (tours across auth states, tours on virtual-scrolled lists) help more than reference docs alone.
Roadmap:
| Version | Focus |
|---|---|
| v1 (now) | React + Vite, visual capture, per-screen multi-tour registry, positioning stability, mobile overrides |
| v2 | Next.js (App + Pages), SSR-safe capture, CRA / webpack support |
| v3 | Richer authoring UI (drag-reorder, live preview), more step-level transitions, i18n-ready tour packs |
Open an issue before any large PR so we can align on design. Small fixes and missing-option PRs don't need an issue first.
This project complements, and does not replace, the react-joyride skill. Both approaches are valid β choose joyride-studio when you want visual authoring, design-system matching, and multi-tour routing; choose plain react-joyride when you prefer full control and already have selectors in hand.
License: MIT.
Skill source: .claude/skills/joyride-studio/SKILL.md (reference for agents)
Template source: .claude/skills/joyride-studio/template/ (files copied into target repos)








