Nuxt

Install StepsKit (Nuxt)

For Nuxt 3 and 4. Nuxt renders on the server by default, which is the one thing that makes this install different from plain Vue — get it wrong and your app crashes during SSR rather than failing quietly.

Prerequisites

  • Your project API key, from the Integrations page in the dashboard.
  • Nuxt 3 or 4.

1. Install the package

npm install stepskit

Create plugins/stepskit.client.ts:

// plugins/stepskit.client.ts
import stepskit from "stepskit";

export default defineNuxtPlugin(() => {
  stepskit.init("YOUR_API_KEY");
});

The .client suffix is not optional. Nuxt runs plugins on both the server and the client, and only the client should be loading a browser SDK. Nuxt reads the suffix from the filename — there is no config to set.

Nuxt auto-registers anything in plugins/, so there is nothing else to wire up.

The package's call queue is doing real work here: the engine is fetched asynchronously, so an identify() call made before it arrives would otherwise be dropped on the floor. The package buffers those calls and replays them the moment the engine loads — which is what makes step 2 safe to write without worrying about ordering.

Without the package: nuxt.config.ts

If you can't add a dependency, or you'd rather keep this out of plugins/:

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: "https://cdn.stepskit.com/stepskit.latest.js",
          async: true,
          "data-api-key": "YOUR_API_KEY",
        },
      ],
    },
  },
});

This emits the tag into the server-rendered HTML, so it starts loading fractionally earlier. The trade-off is that there's no queue stub — an identify() call that runs before the SDK arrives is silently dropped. If you take this route, pass user data as data-user-* attributes on the same tag instead of calling identify().

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.

The cleanest spot is the same client plugin, below the loader you pasted in step 1, watching whatever composable your auth library exposes:

// plugins/stepskit.client.ts
import stepskit from "stepskit";

export default defineNuxtPlugin(() => {
  stepskit.init("YOUR_API_KEY");

  // `useUserSession` here is nuxt-auth-utils; swap in whatever your app uses
  // (`useSupabaseUser`, a Pinia store, your own composable).
  const { user } = useUserSession();

  watch(
    user,
    (current) => {
      if (!current) return;
      stepskit.identify({
        id: current.id,
        email: current.email,
        plan: current.plan,
      });
    },
    { immediate: true },
  );
});

{ immediate: true } covers the case where the session is already resolved when the plugin runs; the watcher covers sign-in afterwards.

Pass whatever your user object already carries — every key 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.

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.

Nuxt 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 useRouter().afterEach() or the page:finish hook.

Guarding other browser-only code. If you add StepsKit calls outside a .client plugin — in a composable or a component that also renders on the server — guard them:

if (import.meta.client) {
  stepskit.identify({ id: user.id });
}

Attributes don't survive a full page load. Client-side navigation is fine — StepsKit keeps user context across it. But a hard reload starts clean, which is why identify belongs in a plugin that runs on every page load rather than in one route's setup.

Content Security Policy. Only relevant if your app sets one (commonly via the nuxt-security module or Nitro route rules):

  • 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 plugin appends an external <script src> rather than running an inline block, you do not need script-src 'unsafe-inline'.

Troubleshooting

document is not defined / 500 on first render. Your plugin is missing the .client suffix. Rename it to plugins/stepskit.client.ts.

Dashboard still says "Not connected". Check the plugin file is directly in plugins/ (Nuxt only auto-registers the top level by default) and that the page you loaded actually rendered client-side.

Connected, but no tour plays. Usually identity. Run validateEnvironment(): if visitorId is null, your watcher isn't firing — check that the composable actually resolves a user on that page.

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 to your project — anywhere your tsconfig already includes.

Next steps