Next.js

App Router, Pages Router, and SSR notes.

App Router

The provider is a client component. Put it in its own file so your layout can stay a server component.

app/providers.tsx
"use client";

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

export function Providers({ children }: { children: ReactNode }) {
  return (
    <CairnProvider flows={flows} router={useAppRouterAdapter()}>
      {children}
      <CairnOverlay />
    </CairnProvider>
  );
}
app/layout.tsx
import { Providers } from "./providers";
import "@cairnkit/ui/styles.css";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body><Providers>{children}</Providers></body>
    </html>
  );
}

Pages Router

Same shape, different adapter. Wrap in _app.

pages/_app.tsx
import type { AppProps } from "next/app";
import { CairnProvider } from "@cairnkit/react";
import { usePagesRouterAdapter } from "@cairnkit/next";
import { CairnOverlay } from "@cairnkit/ui";
import { flows } from "../walkthrough/flows";
import "@cairnkit/ui/styles.css";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <CairnProvider flows={flows} router={usePagesRouterAdapter()}>
      <Component {...pageProps} />
      <CairnOverlay />
    </CairnProvider>
  );
}

Server rendering

The overlay renders nothing on the server and mounts through a portal after hydration, so there is no markup mismatch. resolveAnchor returns null when there is no document rather than throwing, so importing Cairn from a server component is safe.

useTourDeepLink() starts a flow from ?tour=<flowId>, so support can send someone straight into a guide.

"use client";
import { useTourDeepLink } from "@cairnkit/react";

export function DeepLink() {
  useTourDeepLink();   // or useTourDeepLink("guide") for ?guide=
  return null;
}

Known gotchas

  • TypeScript 7. Next 15 rejects the native TS7 compiler. Pin typescript@^5.7 until Next 16.
  • Turbopack and workspace packages. Developing Cairn alongside your app needs transpilePackages in next.config.ts. Not needed when installing from npm.
  • Route changes are asynchronous. Anchors are waited for, so a step whose action navigates just works — see going off-script.