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 stepskitThen 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.