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

Run it in your browser

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.

Next.js โ€” App RouterProvider in a client component, anchors in server components, route-aware steps.walkthrough/flows.tsReact โ€” Vite + react-routerThe ten-line router adapter, a multi-route tour, and the launcher.src/walkthrough/flows.ts

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

Set it up, end to end

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.

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.