React

Vite, react-router, TanStack, or no router at all.

The router adapter

Cairn is not a Next library. Everything route-related — resumeAt, handoffRoutes, pauseRoutes, the route advance rule — goes through one two-method interface.

type RouterAdapter = {
  usePathname(): string;
  navigate(href: string): void;
};

That is the entire surface. Any router is about ten lines.

react-router

src/router-adapter.ts
import { useLocation, useNavigate } from "react-router-dom";
import type { RouterAdapter } from "@cairnkit/react";

export function useReactRouterAdapter(): RouterAdapter {
  const navigate = useNavigate();

  return {
    usePathname: () => useLocation().pathname,
    navigate: (href) => navigate(href),
  };
}

TanStack Router

import { useRouterState, useNavigate } from "@tanstack/react-router";
import type { RouterAdapter } from "@cairnkit/react";

export function useTanStackAdapter(): RouterAdapter {
  const navigate = useNavigate();

  return {
    usePathname: () => useRouterState({ select: (s) => s.location.pathname }),
    navigate: (href) => navigate({ to: href }),
  };
}

No router at all

A single-page app with no routing still works — every route feature simply never fires.

const staticAdapter = {
  usePathname: () => "/",
  navigate: () => {},
};

Mounting

src/App.tsx
import { BrowserRouter } from "react-router-dom";
import { CairnProvider } from "@cairnkit/react";
import { CairnOverlay, TourLauncher } from "@cairnkit/ui";
import "@cairnkit/ui/styles.css";
import { useReactRouterAdapter } from "./router-adapter";
import { flows } from "./walkthrough/flows";

function Shell() {
  return (
    <CairnProvider flows={flows} router={useReactRouterAdapter()}>
      <Routes>{/* ... */}</Routes>
      <CairnOverlay />
      <TourLauncher flowId="onboarding" />
    </CairnProvider>
  );
}

export const App = () => (
  <BrowserRouter>
    <Shell />
  </BrowserRouter>
);

Vite notes

  • Import the stylesheet once, anywhere: import "@cairnkit/ui/styles.css".
  • If you develop Cairn alongside your app via a workspace link, add resolve.dedupe: ["react", "react-dom"] — symlinked packages can otherwise resolve a second copy of React and hooks break.

A complete working example lives in examples/react-vite.