React

Install StepsKit (React)

For React apps built with Vite, Create React App, or any plain React setup. Next.js users — there's a dedicated guide for the App and Pages Routers.

Install the package

npm install stepskit

Then render <StepsKit /> once, at the root of your tree in src/main.tsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { StepsKit } from "stepskit/react";
import App from "./App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <StepsKit apiKey="YOUR_API_KEY" />
    <App />
  </StrictMode>,
);

The component renders nothing. It loads StepsKit once and is StrictMode-safe, so React's double-invoked development mount doesn't inject a second script. Put it above <App /> so the call queue exists before any component can call identify().

Once the engine arrives, StepsKit fetches your project's active tours and waits for instructions.

The queue matters here: the engine loads asynchronously, so an identify() call made during your first render would otherwise arrive before there's anything to receive it. The package buffers those calls and replays them in order the moment the engine is ready.

Identify as you mount

If you already have the user at the root, pass them directly and skip the separate identify call below:

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

The component re-identifies when the values change — passing a new object with the same values does nothing.

Without the component

Any React setup can call init() directly instead — in src/main.tsx, above the render call:

import stepskit from "stepskit";

stepskit.init("YOUR_API_KEY");

Same behaviour; the component just handles mounting for you.

Alternative: no dependency

If you can't add a dependency, save this as src/stepskit.ts and import it once, first, in src/main.tsx. There's no IIFE and no wrapper — an ES module is already a private scope, so nothing leaks onto window.

// Queue API calls made before the SDK finishes loading.
const QUEUED = [
  "identify",
  "setUserAttributes",
  "track",
  "refresh",
  "playTour",
  "stopTour",
  "dismissAnnouncement",
  "on",
  "off",
  "destroy",
  "validateEnvironment",
];

const w = window as unknown as { stepskit?: Record<string, unknown> };
const stepskit = (w.stepskit ??= {});
stepskit._q ??= [];

for (const method of QUEUED) {
  stepskit[method] ??= (...args: unknown[]) =>
    (stepskit._q as unknown[]).push([method, ...args]);
}

// Load the StepsKit SDK asynchronously.
const script = document.createElement("script");
script.async = true;
script.src = "https://cdn.stepskit.com/stepskit.latest.js";
script.dataset.apiKey = "YOUR_API_KEY";
document.head.appendChild(script);

// Marks this file a module, so the consts above stay local to it.
export {};

For a plain JavaScript project, name it src/stepskit.js and delete the three type annotations — as unknown as { stepskit?: Record<string, unknown> }, as unknown[], and : unknown[]. The rest is valid JavaScript as written.

The HTML snippet in index.html also works — at the project root for Vite, or public/index.html for Create React App. Use it only if your app already loads its other third-party scripts (analytics, Sentry) that way; some hosted sandboxes regenerate index.html out from under you.

Identify the user after login

Anywhere you have the user object — your auth callback, a TanStack Query onSuccess, your Zustand/Redux login action — call identify. StepsKit re-evaluates which tours should play now that it knows who the visitor is.

import stepskit from "stepskit";

async function handleLogin(credentials) {
  const user = await login(credentials);

  stepskit.identify({
    id: user.id,
    email: user.email,
    plan: user.plan,
  });
}

TypeScript

Types ship with the package — there's nothing to declare, and nothing to configure.

If you installed one of the alternatives above instead, StepsKit only attaches itself to window, so you need the ambient declaration below. Drop it into src/; Vite and Create React App pick up any .d.ts there automatically.

// src/stepskit.d.ts
// Only needed for the <script> snippet install. If you installed the "stepskit"
// npm package, delete this file — the package ships its own types.
export {};

type StepsKitAttributes = Record<string, string | number | boolean>;

interface StepsKitToursFilteredEntry {
  tourId: string;
  name: string;
  reason:
    | "url_pattern_mismatch"
    | "screen_width_too_narrow"
    | "targeting_failed"
    | "frequency_capped"
    | "inactive";
  detail?: string;
}

interface StepsKitEnvironmentReport {
  apiKey: string; // masked (e.g. "sk_live_a3f...***")
  baseUrl: string;
  visitorId: string | undefined;
  userAttributes: StepsKitAttributes | undefined;
  initialized: boolean;
  isPlaying: boolean;
  currentTourId: string | null;
  toursLoaded: number;
  tooltipsLoaded: number;
  toursFiltered: StepsKitToursFilteredEntry[];
  warnings: string[];
}

declare global {
  interface Window {
    stepskit?: {
      identify(visitor: StepsKitAttributes): Promise<void>;
      setUserAttributes(
        attrs: StepsKitAttributes,
        options?: { autoRefresh?: boolean },
      ): Promise<void>;
      getUserAttributes(): StepsKitAttributes | undefined;
      playTour(tourId: string): Promise<void>;
      stopTour(): void;
      isPlaying(): boolean;
      getTours(): Array<{ id: string; name: string }>;
      refresh(): Promise<void>;
      on(event: string, handler: (...args: unknown[]) => void): void;
      off(event: string, handler: (...args: unknown[]) => void): void;
      dismissAnnouncement(announcementId: string): void;
      destroy(): void;
      validateEnvironment(): StepsKitEnvironmentReport;
      /** No-op today: warns once and discards. Do not wire product events to it. */
      track(event: string, properties?: Record<string, unknown>): void;
    };
  }
}

See the JavaScript API reference for what each method does.

Without a backend user

For signed-out routes you can simply skip identify. Tours targeted at anonymous visitors will still play, and "show once" rules fall back to a session-scoped check.