Interactive explainers for React: step-through players, guided tours, scroll-driven lessons and glossary tooltips, all on one cursor.
Live demos · API reference · Try it on StackBlitz
You want to show how something works: an algorithm, a protocol, a pipeline, your product's new feature. Today that means gluing together a tour library, a scrollytelling library, a hand-rolled play/pause/scrub player, and tooltips, each with its own state model, and then keeping them in sync.
stepwise starts from one observation: frames in a trace, steps in a tour
and chapters in a lesson are all the same thing, a clamped cursor over an
ordered sequence. So there is one primitive (useSteps) and a few policies
on top of it:
| You're building | Use | You get |
|---|---|---|
| An algorithm or protocol walkthrough | useTrace + PlayerBar |
play/pause/scrub, speed, keyboard, #frame=12 deep links, reduced-motion aware autoplay, a scrubber colored by step tone |
| An explainer the reader can poke at | useSimulation |
the trace is computed from an input; edit it mid-playback and the playhead stays on the same step; seeded randomness, so links replay exactly |
| A live feed: logs, streams, agent steps | useTrace({ follow }) + PlayerBar live |
new frames play in while you watch; scrub back to inspect one and the stream doesn't yank you away; "Go live" returns |
| "Pause and predict" exercises | holdAt checkpoints + useEvaluator |
autoplay stops where you choose; grade the reader's guess with feedback, async checks, attempt counts |
| A side-by-side comparison | useSyncedTracks |
N traces under one playhead, aligned on shared sync points |
| A product tour / onboarding | useGuidedTour + TourSpotlight + TourCard |
steps that change app state (onEnter), gates that wait for the user to actually do the thing, branches, skip conditions |
| A show-once feature tutorial | useDiscoveries + a tour |
a persisted "seen it" ledger (localStorage, cookie, or your API) |
| A long-form scroll lesson | useChapters + ChapterNav |
viewport-center chapter tracking, nested chapters, progress |
| Jargon-heavy prose | Term + GlossaryAppendix |
recursive definition tooltips and an auto-built appendix |
Because they share one cursor they compose: the flagship lesson demo is a scroll lesson whose chapters seek an embedded Dijkstra player, with a guided tour over the top, and every cursor shown live.
npm install @sphynxoid/stepwise"use client";
import { createTrace, useTrace, PlayerBar } from "@sphynxoid/stepwise";
import "@sphynxoid/stepwise/styles.css"; // skip this if you use Tailwind; see "Styling"
// Build the trace once, at module scope. Your own data rides along on each frame.
const frames = createTrace<{ arr: number[] }>()
.step({ tone: "neutral", label: "Start", arr: [3, 1, 2] })
.step({ tone: "write", label: "Swap 3 and 1", arr: [1, 3, 2] })
.step({ tone: "write", label: "Swap 3 and 2", arr: [1, 2, 3] })
.step({ tone: "good", label: "Sorted", arr: [1, 2, 3] })
.build();
export function SortDemo() {
const { step, player, toneAt, titleAt } = useTrace(frames, { autoPlay: true, syncHash: "frame" });
return (
<figure>
<p>{step!.arr.join(" ")}: {step!.label}</p>
<PlayerBar player={player} toneAt={toneAt} titleAt={titleAt} />
</figure>
);
}How you draw each frame is up to you: DOM, SVG, canvas, or GSAP via the
PlayerEngine adapter. stepwise owns the timeline, not the pixels.
Instead of recording frames by hand, write the algorithm as a generator and
give each step a key naming its moment. useSimulation reruns it whenever
the input changes, and the playhead stays on the same moment, even mid-playback
(try it):
const insertionSort = function* (input: number[]) {
const a = [...input];
for (let i = 1; i < a.length; i++)
for (let j = i; j > 0 && a[j - 1] > a[j]; j--) {
[a[j - 1], a[j]] = [a[j], a[j - 1]];
yield { key: `swap ${i}.${j}`, tone: "write" as const, label: `swap at ${j}`, arr: [...a] };
}
};
function Explorer() {
const [input, setInput] = useState([5, 2, 8, 1]);
const sim = useSimulation(insertionSort, input, { autoPlay: true });
// render sim.step.arr, let the reader edit `input`, add <PlayerBar player={sim.player} … />
}Need randomness? Use the seeded ctx.rng the generator receives, never
Math.random, and the same input plus seed always gives the same trace.
Add syncInput to put the input in the URL so links replay the exact run,
holdAt to pause at checkpoints, and useEvaluator to grade a guess.
stepwise isn't a drop-in replacement for any single tool below. Its point is that one model covers the whole set. Choose honestly:
| Step player | Product tour | Scroll chapters | Glossary | Composes | |
|---|---|---|---|---|---|
| stepwise | ✅ | ✅ | ✅ | ✅ | ✅ one cursor |
| react-joyride / Shepherd / driver.js | – | ✅ | – | – | – |
| scrollama / react-scrollama | – | – | ✅ | – | – |
| Motion Canvas / Remotion | renders video (frame-accurate) | – | – | – | – |
- Only need a tour? react-joyride and Shepherd are mature, with years of
edge cases handled. stepwise's tour wins when steps must mutate app state
or wait on real user actions (
onEnter,gate), or when it shares a page with a player. It can also present through driver.js via the adapter. - Only need scroll triggers? scrollama is tiny and framework-agnostic.
- Want a video? Motion Canvas and Remotion render frame-accurate ones. stepwise is for explainers the reader drives: pause, scrub, deep-link to a frame. For a step-by-step clip of one (a GIF for a post), see exporting a GIF or video.
The hooks are headless. The optional components (PlayerBar, TourCard,
Term, …) can be styled three ways:
| Your app | Do this |
|---|---|
| No Tailwind | import "@sphynxoid/stepwise/styles.css"; once: precompiled, light/dark, themable via --stepwise-* variables |
| Tailwind + shadcn/ui | npx shadcn@latest add https://stepwise.thesphynx.net/r/player-bar.json copies the component source into your repo (hooks stay on npm) |
| Tailwind, components from npm | add @source "../node_modules/@sphynxoid/stepwise/dist"; to your CSS entry |
Details and the "everything renders unstyled" checklist: docs/TAILWIND-SETUP.md.
- React 18 and 19, SSR-safe (
"use client"modules, nowindowat import), tested in Next.js (App Router) and Vite. - Small dependency surface: React plus
clsx/tailwind-merge. GSAP and driver.js integrations are optional subpath imports. - Accessible by default: keyboard control, focus-trapped tour card, ARIA slider scrubber,
prefers-reduced-motionrespected. - Experimental: branching walks.
@sphynxoid/stepwise/experimentalhasuseWalk, where steps can wait for the reader's choice and branch on it (the trace becomes a graph; the path is still scrubbable and linkable), andfromGraphfor automata you can run and draw. See /automaton. It may change in a minor release. - Works with AI assistants: the package ships
LLMS.md, and the site serves/llms.txtand/llms-full.txt, so an assistant that has never seen stepwise can read the patterns and pitfalls instead of guessing. - ESM only.
- Pre-1.0: the API may still change between minor versions. Breaking changes are listed in the CHANGELOG. Feedback on the API is the most useful thing you can send right now.
packages/stepwise: the library (package README has the full API tour)apps/demo: the demo site (Next.js); every page has a "View source" panel showing its real codeexamples/vite-starter: the smallest real app, installed from npmexamples/next-mdx: an interactive article in Next.js + MDX (editable player, predict mode, glossary)examples/export-video: record your explainer as a GIF (or frames for an MP4)docs/RECIPES.md: Next.js + MDX, shareable links, grading a checkpoint, testing, exporting a GIF or videodocs/ARCHITECTURE.md: the layering, ports/adapters, and the algorithms with the reasoning behind eachAGENTS.md: the contributor's operating manual (commands, invariants, gotchas)
npm install
npm run demo # builds the library, starts the demo
npm run test # unit tests (or: cd packages/stepwise && npx vitest run)Contributions welcome. Start with CONTRIBUTING.md.
MIT