SvelteKit
Install StepsKit (SvelteKit)
For SvelteKit 2. If you're on plain Svelte + Vite with no SvelteKit, skip to that section — the entrypoint is different.
Prerequisites
- Your project API key, from the Integrations page in the dashboard.
- SvelteKit 2 (Svelte 4 or 5 — the package doesn't care).
- If your app enables SvelteKit's CSP config, read Content Security Policy first. It's the one thing here that can block the install outright.
1. Install the package
npm install stepskitCall init() in your root layout, src/routes/+layout.svelte:
<!-- src/routes/+layout.svelte -->
<script>
import stepskit from "stepskit";
stepskit.init("YOUR_API_KEY");
</script>
<slot />init() is SSR-safe — it's a no-op when there's no window, so it does
nothing during the server render and runs on hydration. It's also idempotent,
so there's no browser guard or onMount to write.
The root layout wraps every page and mounts once per full page load, never re-running on client-side navigation. That's what you want.
The package's call queue is doing real work here: the engine loads
asynchronously, so an identify() call from a child route 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.
Plain Svelte + Vite
Without SvelteKit there is no root layout. Call init() at the top of
src/main.ts instead, above the Svelte imports — see the
React guide for the same pattern.
Without the package
If you can't add a dependency, paste the snippet into src/app.html, just
before </body>:
<script>
(function () {
var stepskit = window.stepskit = window.stepskit || {};
stepskit._q = stepskit._q || [];
// Queue API calls made before the SDK finishes loading.
var methods = ['identify', 'setUserAttributes', 'track', 'refresh', 'playTour', 'stopTour', 'dismissAnnouncement', 'on', 'off', 'destroy', 'validateEnvironment'];
methods.forEach(function (method) {
stepskit[method] = stepskit[method] || function () {
stepskit._q.push([method].concat([].slice.call(arguments)));
};
});
// Load the StepsKit SDK asynchronously.
var script = document.createElement('script');
script.async = true;
script.src = 'https://cdn.stepskit.com/stepskit.latest.js';
script.setAttribute('data-api-key', 'YOUR_API_KEY');
var first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(script, first);
})();
</script>Your src/app.html should end up looking like this:
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
<!-- StepsKit loader goes here -->
</body>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.
Resolve the user in src/routes/+layout.server.ts and identify from
+layout.svelte:
// src/routes/+layout.server.ts
export const load = async ({ locals }) => {
return { user: locals.user };
};<!-- src/routes/+layout.svelte -->
<script>
import stepskit from "stepskit";
let { data, children } = $props();
stepskit.init("YOUR_API_KEY");
$effect(() => {
if (!data.user) return;
stepskit.identify({
id: data.user.id,
email: data.user.email,
plan: data.user.plan,
});
});
</script>
{@render children()}No browser guard needed: +layout.svelte runs on the server too, but every
method on the package is a no-op there rather than a crash. (If you installed
without the package, you do need one — window doesn't exist during SSR.)
On Svelte 4, use onMount with $props replaced by export let data — the
StepsKit call is identical.
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.
SvelteKit notes
Route changes need no extra wiring. StepsKit patches history.pushState
and replaceState and listens for popstate, so it sees SvelteKit navigation
on its own. You don't need afterNavigate from $app/navigation.
src/app.html is not reactive. It renders once per full page load. That's
exactly the lifetime you want for a global script — don't try to move it into a
component.
Content Security Policy
SvelteKit is unusual in shipping first-class CSP generation in
svelte.config.js. If you use it, this is the part to get right.
StepsKit needs:
script-src:https://cdn.stepskit.com, plus'unsafe-inline'for the loader block aboveconnect-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.
The conflict: browsers ignore 'unsafe-inline' in any directive that also
carries a hash or a nonce. SvelteKit "will augment the specified directives with
nonces or hashes (depending on mode) for any inline styles and scripts it
generates" — so once it touches style-src, the 'unsafe-inline' StepsKit
needs is silently disabled and the tour engine renders unstyled.
Three ways out:
-
Nonce the loader instead of allowing inline scripts.
src/app.htmlsupports a%sveltekit.nonce%placeholder, so you can hand SvelteKit's nonce to the StepsKit block directly:<script nonce="%sveltekit.nonce%"> // …the StepsKit loader… </script>This solves
script-srccleanly. It does not solvestyle-src— the embed injects its stylesheet at runtime, and you can't nonce that. -
Keep
style-srcout of SvelteKit's CSP management. Set StepsKit'sstyle-src 'unsafe-inline'in a header you control, and let SvelteKit manage onlyscript-src. This is the part you actually have to solve. -
Load the SDK without an inline block. Use a plain external tag in
src/app.html—<script async src="…" data-api-key="…">— soscript-srcneeds no'unsafe-inline'at all. You lose the queue stub, so pass user data asdata-user-*attributes rather than callingidentify().
Note that auto mode uses nonces for dynamically rendered pages and hashes for
prerendered ones, so a site with both will hit this on the prerendered half even
if the dynamic half looks fine.
Troubleshooting
Dashboard still says "Not connected". Check the snippet is in
src/app.html and not in a +layout.svelte — a layout re-renders and would
load the SDK repeatedly.
window is not defined during build or SSR. A StepsKit call is running
server-side. Guard it with browser from $app/environment, or move it into
onMount / $effect.
Tour renders as unstyled text. Your CSP is stripping
style-src 'unsafe-inline' — almost always SvelteKit's csp config in hash or
nonce mode. See above.
CSP console errors about the inline script. Same cause, script-src side.
Use option 2 above.
Connected, but no tour plays. Usually identity. Run
validateEnvironment(): if visitorId is null, your identify isn't running —
check the browser guard isn't short-circuiting it permanently.
Next steps
- JavaScript API reference — every method on
window.stepskit. - User attributes — what you can target on.
- Frequency capping — how "show once" works.