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 stepskitCreate 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:
- Dashboard — the project's Integrations page flips to Connected.
- Console, on a signed-in page — run
window.stepskit.validateEnvironment(). You want a non-nullvisitorIdand a populateduserAttributes. A nullvisitorIdmeans step 2 never ran. - Network —
cdn.stepskit.com/stepskit.latest.jsreturns200.
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.comconnect-src:https://stepskit.comstyle-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
- JavaScript API reference — every method on
window.stepskit. - User attributes — what you can target on.
- Frequency capping — how "show once" works.