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"'.Every file you end up with, for both frameworks. These are trimmed from the examples in the repository, which CI typechecks, builds and runs a browser audit against — so they cannot quietly drift from code that works.
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.