Interactive

Playground

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.

idle

Projects

Onboarding revampUpdated today
Billing migrationUpdated today
Design tokensUpdated today
walkthrough/flows.ts
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,
  },
  ],
});

The launcher

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.

position
bottom-right
icon

icon 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.

app/library.tsx
<TourLauncher
  flowId="playground"
  label="Watch guide"
  position="bottom-right"
  pulse
/>

Proving it stays correct

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.

Terminal
$ 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"'.

Set it up, end to end

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.

Vite, react-router. Works the same with TanStack Router, or no router at all.
Terminal
npm i @cairnkit/core @cairnkit/react @cairnkit/ui
npm i -D @cairnkit/cli

Every target, declared once. This is the file that turns a renamed element into a compile error.

walkthrough/anchors.ts
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" },
});

What makes it work

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.

  1. Declare the anchors. One object, referenced by name everywhere else. This is the file that makes a renamed element a compile error rather than a support ticket.
    walkthrough/anchors.ts
    export const anchors = defineAnchors({
      demo: { nav: "demo.nav", create: "demo.create", save: "demo.save" },
    });
  2. Apply them. Spread onto the real element. Nothing is queried by class or id, so restyling cannot break a tour.
    app/projects.tsx
    <button {...anchor(anchors.demo.create)}>New project</button>
  3. Write the flow. Plain data — the panel at the top of this page prints exactly this as you change the controls.
  4. Mount the provider and the overlay. Once, near the root.
    app/providers.tsx
    <CairnProvider flows={flows} router={useAppRouterAdapter()}>
      {children}
      <CairnOverlay />
    </CairnProvider>

Things that will catch you out

Next: install it, or read how modals are handled and how the build catches drift.