# Install StepsKit in Remix or React Router 7

> Add StepsKit to a Remix or React Router 7 framework-mode app with the stepskit npm package in app/root.tsx, identify users from the root loader, and pass a CSP nonce.

*Source: https://stepskit.com/docs/install/remix*

For Remix v2 and React Router 7 in framework mode. Remix was merged into React
Router 7, so if your imports come from `react-router` rather than
`@remix-run/react` you're on the newer one — the install below is the same
either way, and only the import paths differ.

## Prerequisites

- Your project API key, from the **Integrations** page in the
  [dashboard](/app).
- Remix v2 or React Router 7 in framework mode. If you're using React Router as
  a plain client-side library inside a Vite app, follow the
  [React guide](/docs/install/react.md) instead.

## 1. Install the package

```bash
npm install stepskit
```

Render `<StepsKit />` in the `Layout` export in `app/root.tsx`, after
`<Scripts />`:

```tsx
// app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
import { StepsKit } from "stepskit/react";

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
        <StepsKit apiKey="YOUR_API_KEY" />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}
```

On Remix v2, import from `@remix-run/react` instead of `react-router`. Nothing
else changes.

`Layout` wraps every route and mounts once per full page load, so StepsKit
loads once and stays put across client-side navigation.

### About ordering

The package installs its call queue synchronously, before the engine finishes
downloading — so any `identify()` or `playTour()` call you make is buffered and
replayed in order rather than dropped. There's no window where the API is
missing, and nothing to guard with `window.stepskit?.`.

## 2. Identify the user

**This is half the install, not a nice-to-have.** Without an `id`, show-once
capping degrades to a per-tab check and any tour with a "filtered" audience is
hidden outright.

Return the user from the root loader, then hand it to the same component —
there is no separate identify call to write:

```tsx
// app/root.tsx
export async function loader({ request }: Route.LoaderArgs) {
  const user = await getUserFromSession(request);
  return { user };
}
```

```tsx
// app/root.tsx — inside Layout
import { useRouteLoaderData } from "react-router";

const data = useRouteLoaderData<{ user?: User }>("root");
const user = data?.user;

<StepsKit
  apiKey="YOUR_API_KEY"
  user={user ? { id: user.id, email: user.email, plan: user.plan } : undefined}
/>;
```

The component identifies the visitor when `user` appears and re-identifies when
its **values** change — passing a new object with the same values does nothing,
so a re-rendering loader won't cause repeat calls. It's safe on the server too:
everything it does is client-only.

Pass whatever your user object already carries; every key becomes targetable and
there's no fixed schema to conform to. When the user switches workspace or
changes plan, the loader re-runs and the new values are picked up.

## 3. Verify it worked

Load any page of your app, then:

1. **Dashboard** — the project's Integrations page flips to **Connected**.
2. **Console, on a signed-in page** — run
   `window.stepskit.validateEnvironment()`. You want a non-null `visitorId` and
   a populated `userAttributes`. A null `visitorId` means step 2 never ran.
3. **Network** — `cdn.stepskit.com/stepskit.latest.js` returns `200`.

`validateEnvironment()` also returns a `toursFiltered` table that says exactly
why any given tour isn't showing.

## Remix notes

**Route changes need no extra wiring.** StepsKit patches `history.pushState`
and `replaceState` and listens for `popstate`, so it sees client-side
navigation on its own. You don't need `useLocation` plumbing or a `refresh()`
call.

**`root.tsx` runs on the server.** `<StepsKit />` is safe there — it renders
nothing and does all its work on the client. Your own `window` access isn't;
keep that in effects or event handlers.

**Content Security Policy.** Remix and React Router thread a nonce through their
script components, and StepsKit takes one the same way:

```tsx
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<StepsKit apiKey="YOUR_API_KEY" nonce={nonce} />
```

StepsKit needs:

- `script-src`: `https://cdn.stepskit.com`
- `connect-src`: `https://stepskit.com`
- `style-src`: `'unsafe-inline'` — **required**. The embed injects its entire
  stylesheet as a `<style>` element when the script evaluates. Without this the
  tour engine renders completely unstyled rather than merely off-brand.
- `img-src`: `https://*.supabase.co` — only if your steps use images.

Because StepsKit loads via an external `<script src>` and not an inline block,
you do **not** need `script-src 'unsafe-inline'`. The `nonce` prop is only
needed if your policy requires a nonce on every script.

## Troubleshooting

**Dashboard still says "Not connected".** Check `<StepsKit />` is in `Layout`
and not in the default `App` export — only `Layout` renders the document shell.

**`window is not defined` during SSR.** A StepsKit call escaped an effect. Move
it inside `useEffect` or a handler.

**Connected, but no tour plays.** Usually identity. Run
`validateEnvironment()`: if `visitorId` is null, `useRouteLoaderData("root")`
probably isn't returning a user — confirm the root loader actually runs on that
route.

**Tour renders as unstyled text.** Your CSP is missing
`style-src 'unsafe-inline'`.

**`Property 'stepskit' does not exist on type 'Window'`.** Add the ambient
declaration from the [API reference](/docs/api.md#typescript) to `app/`.

## Next steps

- [JavaScript API reference](/docs/api.md) — every method on `window.stepskit`.
- [User attributes](/docs/concepts/user-attributes.md) — what you can target on.
- [Frequency capping](/docs/concepts/frequency-capping.md) — how "show once" works.
