A real tour, running against a real UI, driven by the same flow object you would write in your own repo. Change an option, run it again, then copy the result.
export const flow = defineFlow({
id: "playground",
version: 1,
entryRoute: "/playground",
steps: [
{
anchor: anchors.demo.nav,
title: "Start here",
body: "Anchors are referenced by name.",
placement: "bottom",
padding: 8,
},
{
anchor: anchors.demo.create,
title: "Then here",
body: "Rename this button and the build fails.",
placement: "bottom",
padding: 8,
},
{
anchor: anchors.demo.save,
title: "Done",
body: "Three steps, defined as data.",
placement: "bottom",
padding: 8,
},
],
});How a user starts a tour themselves. Pick a corner and an icon โ the launcher below is real and fixed to your viewport, so you are looking at the actual component, not a picture of it.
bottom-righticon is a ReactNode โ an SVG, an emoji, your own logo, anything React renders. These five are just examples.
It is a real launcher, fixed to the viewport โ look at the bottom right of your screen. Clicking it calls start(flowId) and runs whichever scenario is selected above, currently a plain tour. It hides itself while a tour is running, so it never floats over its own spotlight.
<TourLauncher
flowId="playground"
label="Watch guide"
position="bottom-right"
pulse
/>Running a tour is the easy half. The half that matters is what happens six months later when someone renames the button it points at. Three layers, each catching what the one before it cannot โ with the output each actually produces.
CatchesA renamed or misspelled anchor, flow id, event or action name.
Anchors are declared once and registered, so every id in the API narrows to your own literals. There is no string to get wrong.
$ tsc --noEmit
src/cairn/flows.ts(45,44): error TS2345: Argument of type
'"invite:close-setings"' is not assignable to parameter of type
'"invite:close-settings"'.The examples from the repository, running for real โ install, edit, break something. These are generated from the same projects CI typechecks and builds, so they cannot drift from code that works.
walkthrough/flows.tsReact โ Vite + react-routerThe ten-line router adapter, a multi-route tour, and the launcher.src/walkthrough/flows.tsNext.js takes about 30 seconds to boot in a browser container; the Vite one starts in a few. Both install from npm exactly as your own project would.
Every file you end up with, for the two setups the repository ships as runnable examples. CI typechecks, builds and runs a browser audit against both, so they cannot quietly drift from code that works. Pages Router, TanStack and no-router-at-all are the same four files with a different adapter: Next.js and React cover each one.
npm i @cairnkit/core @cairnkit/react @cairnkit/ui
npm i -D @cairnkit/cliEvery target, declared once. This is the file that turns a renamed element into a compile error.
import { defineAnchors } from "@cairnkit/core";
export const anchors = defineAnchors({
nav: { library: "nav.library" },
library: { tabs: "library.tabs", newCta: "library.new-cta" },
form: { title: "form.title", submit: "form.submit" },
});Four pieces, and only the first two are things you write. The demo above is wired exactly like this โ there is no playground-only code path.
export const anchors = defineAnchors({
demo: { nav: "demo.nav", create: "demo.create", save: "demo.save" },
});<button {...anchor(anchors.demo.create)}>New project</button><CairnProvider flows={flows} router={useAppRouterAdapter()}>
{children}
<CairnOverlay />
</CairnProvider>Resolution skips hidden and zero-size elements, so a step pointing at something behind a collapsed accordion waits rather than resolving. Open it first, or mark the step optional.
Every later step is behind the dialog. Close it in onExit โ try the third scenario above with that line removed and you will watch the spotlight land on covered layout.
Closing a dialog they opened is cleanup. Clicking the button a step is teaching is not โ they learn nothing, and the tour breaks the moment the handler changes.
In the shell it follows people onto pages its guide says nothing about. Put it on the view the tour describes.
This page runs its own provider so the playground cannot mark the site's own tour as complete. Pass a key to localStoragePersist("your-key") whenever a second provider exists.
Placement, padding and beacon are ordinary fields on a step. There is no runtime config object to learn.
Next: install it, or read how modals are handled and how the build catches drift.