Product tours that fail your build, not your users.

Every other tour tool targets a CSS selector, so a rename breaks it silently and a customer finds out first. cairnkit puts tours in your repo as typed data and fails CI the moment one points at UI that no longer exists.

$npm i @cairnkit/core @cairnkit/react @cairnkit/ui
Try the playground
5.4 kbheadless, gzipped
0runtime dependencies
~0.1sCI check, 700 files
3layers of drift defence

Your stack

Use cairnkit with Next.js App Router

One client component holds the provider. Anchors still work in server components, because anchor() returns plain props.

  • React 18 & 19
  • Next.js 14, 15, 16
  • Server-rendered
  • Vite & any bundler
app/providers.tsx
"use client";

import { CairnProvider } from "@cairnkit/react";
import { useAppRouterAdapter } from "@cairnkit/next";
import { CairnOverlay } from "@cairnkit/ui";
import { flows } from "@/walkthrough/flows";

export function Providers({ children }) {
  return (
    <CairnProvider flows={flows} router={useAppRouterAdapter()}>
      {children}
      <CairnOverlay />
    </CairnProvider>
  );
}
Read the setup for Next.js App Router

The whole idea

A broken tour should break the build

Someone deletes a button. Every other tool keeps pointing at nothing until a user reports a spotlight over blank space. This runs in about a second, in CI.

$ cairnkit check

✗ cairnkit check failed

   1 anchor(s) are registered but never applied to an element  [anchors-applied]
      - questions.save  (breaks "create-questions")  src/walkthrough/flows.ts:35
      Spread {...anchor(...)} on the element, or remove the anchor and the step pointing at it.

Comparison

What happens when the UI changes

Not a speed benchmark — cairnkit is not faster than driver.js. This is the axis that actually costs you: what each tool does the day someone edits a component.

driver.jsShepherdPendocairnkit
Targets elements byCSS selectorCSS selectorVisual pickerTyped anchor
You rename a classBreaks silentlyBreaks silentlyBreaks silentlyWon't compile
You delete the elementBreaks silentlyBreaks silentlyBreaks silentlyFails CI
Element stops renderingBreaks silentlyBreaks silentlyBreaks silentlyFails the audit
Step inside a modalOften breaksOften breaksWorksWorks

How it works

Four steps, then it is yours

No visual editor, no separate dashboard to keep in sync. Tours live next to the code they describe and move with it.

Declare your anchors

01

One registry. Rename a key and every flow that used it stops compiling.

walkthrough/anchors.ts
export const anchors = defineAnchors({
  questions: { save: "questions.save" },
});

Mark the elements

02

A single spread. Your components import nothing else from cairnkit.

QuestionsPage.tsx
<button {...anchor(anchors.questions.save)}>
  Save
</button>

Write the flow

03

Steps are data, not components. Reordering is an array edit.

walkthrough/flows.ts
defineFlow({
  id: "create-questions",
  version: 1,
  entryRoute: "/questions",
  steps: [{ anchor: anchors.questions.save,
            title: "Save it",
            advanceOn: { type: "click" } }],
});

Wire the check into CI

04

Now a deleted button fails the build instead of a customer's onboarding.

package.json
"scripts": {
  "lint": "eslint . && cairnkit check"
}

Try it

Run one, right now

A real tour against a real UI. Change the placement, padding and advance rule, watch the spotlight move, then copy the flow that produced it. Nothing to install.

Open the playground

Real users

Nobody follows the script

They click ahead, wander into a different flow, or open a modal you did not plan for. Most tours die at that point. cairnkit expects it.

They didClicked the button before the guide got there

cairnkit doesCatches up, forward only

resumeAt

They didTook a different route to the same goal

cairnkit doesHands over to the guide that covers it

handoffRoutes

They didWandered somewhere no guide covers

cairnkit doesSleeps and keeps their place

pauseRoutes

They didOpened a modal you did not plan for

cairnkit doesPortals into the dialog, survives focus traps

automatic

They didLeft a modal with the next target behind it

cairnkit doesRuns your onExit, waits for the close, then measures

onExit

Packages

Small, and mostly optional

Take the engine alone and drive it with your own components, or take the overlay too. Headless lands at 5.4 kb — about what driver.js costs, for rather more.

5.4 kbHeadlesscore + react + next. Bring your own overlay.
14.9 kbEverythingAdds the overlay and @floating-ui/dom.

cairnkit cloud

The check tells you it broke. It cannot tell you what it cost.

cairnkit check catches a tour pointing at a button that no longer exists. It has nothing to say about the tour that works perfectly and still loses two thirds of people at step three — that one is not a bug in your repo, and no compiler will ever find it.

app/providers.tsx
import { sendToCloud } from "@cairnkit/cloud";

<CairnProvider flows={flows} onEvent={sendToCloud({ key })}>

You already pass onEvent. Pointing it at cloud is one import and 1.7 kb, and what comes back is completion rate per flow, the step people quit on, and a Slack message the moment an anchor goes missing in production. Point it at PostHog instead and the library does not care — this is an option, not the plan.

Stop shipping broken tours

MIT licensed, 2.3 kb of engine, and a CI check that tells you before your users do. Five minutes to a working tour; an afternoon to one that cannot silently break.

Get started
$ cairnkit check

✓ cairnkit check · 12 anchors, all applied, no route conflicts

$ echo $?
0