Vue

Install StepsKit (Vue)

For Vue 3 single-page apps built with Vite or Vue CLI. Using Nuxt? It renders on the server, so it needs a different install — go there instead.

Prerequisites

  • Your project API key, from the Integrations page in the dashboard.
  • Vue 3. Vue 2 works too — the package is framework-agnostic — but the file paths below assume Vue 3.

1. Install the package

npm install stepskit

Then call init() once, first, in src/main.ts:

import stepskit from "stepskit";

stepskit.init("YOUR_API_KEY");

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

createApp(App).use(router).mount("#app");

Keep it above the Vue imports. It runs at module-evaluation time, so StepsKit fires once before Vue mounts — nothing to put in a lifecycle hook, and nothing that can run twice.

If your lint rules forbid statements between imports, move the call into its own src/stepskit.ts and side-effect-import that file first:

import stepskit from "stepskit";

// Call once, at your app's entry point. SSR-safe and idempotent.
stepskit.init("YOUR_API_KEY");

That ordering matters because of the call queue. The engine loads asynchronously, so an identify() call made while your app is booting would otherwise arrive before there's anything to receive it. The package buffers those calls and replays them in order when the engine is ready.

Vue CLI instead of Vite

The entrypoint is src/main.js rather than src/main.ts; everything else is identical. Note that Vue CLI's HTML shell lives at public/index.html, whereas Vite's is index.html at the project root — this trips people up when they follow the script tag guide instead.

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.

Call identify wherever your app resolves the signed-in user — a Pinia store action, your login handler, or a router navigation guard:

import { defineStore } from "pinia";
import stepskit from "stepskit";

export const useAuthStore = defineStore("auth", {
  actions: {
    async signIn(credentials) {
      this.user = await api.login(credentials);

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

Call it once per page load, as soon as auth resolves — not on every route change. Pass whatever your user object already carries; every key you pass becomes targetable, and there's no fixed schema to conform to. Re-call it when the user switches workspace or changes plan, so what you target on doesn't go stale.

If your app has signed-out routes, just skip identify there. Tours aimed at everyone still play.

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. Networkcdn.stepskit.com/stepskit.latest.js returns 200.

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

Vue notes

Route changes need no extra wiring. StepsKit patches history.pushState and replaceState and listens for popstate, so it sees Vue Router navigation on its own. You don't need to call refresh() in an afterEach hook.

Hash-mode routing breaks URL targeting. StepsKit compares the URL pathname, and deliberately ignores hash and query changes so a tour isn't torn down by unrelated churn. With createWebHashHistory() every route shares the same pathname (/), so a tour scoped to a URL pattern will never match the route you meant. Use createWebHistory() if you target tours by URL.

No SSR concerns. A Vite or Vue CLI SPA has no server render — and init() is a no-op on the server anyway, so it's safe either way.

Content Security Policy. Only relevant if your app sets one:

  • 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 the package appends an external <script src> rather than running an inline block, you do not need script-src 'unsafe-inline'. If your CSP uses nonces, pass one: stepskit.init(key, { nonce }).

Troubleshooting

Dashboard still says "Not connected". init() never ran. Check that it's actually reached in src/main.ts — and if you put it in its own src/stepskit.ts, that the side-effect import has no named bindings, or a bundler may tree-shake it away.

Connected, but no tour plays. Usually identity. Run validateEnvironment(): if visitorId is null, identify() isn't being called. If it's set, read the toursFiltered table for the reason.

Tour plays on the wrong route, or never on the right one. You're likely on hash-mode routing — see above.

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

Property 'stepskit' does not exist on type 'Window'. You're reading the global instead of the typed import — use import stepskit from "stepskit". If you deliberately installed without the package (see below), add the ambient declaration from the API reference to src/; Vite picks up any .d.ts under src/ automatically.

Without the package

If you can't add a dependency, save this as src/stepskit.ts instead and side-effect-import it first from src/main.ts:

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

Next steps