A Vite-scaffolded Vue app has one index.html that never touches a server render step, so there is no hydration mismatch to guard against and no framework-specific script-loading prop to configure. The script tag goes in index.html like it would on a plain HTML page, and window.OperatorStack is available the moment your app mounts. This guide covers the setup, a Composition API form, and what Vue Router does automatically once the script loads.
Add the OperatorStack script tag to index.html before </body>. Use the data-os-widget="waitlist" div for the default form, or call window.OperatorStack.joinWaitlist() from a ref()-backed component for a custom UI. Vue Router's history-mode navigation is tracked automatically since OperatorStack patches history.pushState.
Step 1: Add the Script Tag to index.html
A Vite Vue app (and a Vue CLI app before it) serves one static index.html that never re-renders server-side. Add the script there once.
Open index.html and add the script before </body>. Replace pk_your_project_key with your project key from the OperatorStack dashboard.
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Your App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script src="https://operatorstack.dev/os.js" data-project="pk_your_project_key"></script>
</body>
</html>
There is no build-time transform and no Vue plugin to register. The browser executes the script the same way it would on any static page, after your Vue bundle has already started mounting.
Step 2: Use the Default Widget Div
For a standard signup form with no custom styling logic, drop the widget div directly in a template. OperatorStack scans for it once the script loads and renders the form inside a Shadow DOM, so it never collides with your app's CSS.
Render the widget div in any single-file component.
<!-- src/components/WaitlistSection.vue -->
<template>
<section>
<h2>Join the waitlist</h2>
<div data-os-widget="waitlist"></div>
</section>
</template>
No onMounted, no template ref, no cleanup logic. The script tag from Step 1 scans the DOM for data-os-widget attributes once it initializes, regardless of which component the div lives in.
Step 3: Build a Custom Form with the Composition API
If the default widget does not match your design system, call the SDK directly from a component built around ref() and reactive().
Create a WaitlistForm.vue component that calls joinWaitlist on submit.
<!-- src/components/WaitlistForm.vue -->
<script setup lang="ts">
import { ref } from "vue";
declare global {
interface Window {
OperatorStack: {
ready: Promise<void>;
joinWaitlist: (params: {
email: string;
name?: string;
}) => Promise<{ success: boolean; referral_code: string; referral_link: string }>;
getShareLinks: (
code: string,
opts?: { text?: string },
) => { twitter: string; linkedin: string; copy: string; email: string; whatsapp: string };
trackEvent: (name: string, data?: Record<string, unknown>) => void;
};
}
}
const email = ref("");
const loading = ref(false);
const shareLinks = ref<{ twitter: string; copy: string } | null>(null);
async function handleSubmit() {
loading.value = true;
await window.OperatorStack.ready;
const result = await window.OperatorStack.joinWaitlist({ email: email.value });
window.OperatorStack.trackEvent("waitlist_signup", { source: document.referrer || "direct" });
const links = window.OperatorStack.getShareLinks(result.referral_code, {
text: "I just joined the waitlist, check it out:",
});
shareLinks.value = { twitter: links.twitter, copy: links.copy };
loading.value = false;
}
</script>
<template>
<div v-if="shareLinks">
<p>You are on the list. Share to move up.</p>
<a :href="shareLinks.twitter">Share on Twitter</a>
<button @click="navigator.clipboard.writeText(shareLinks.copy)">Copy referral link</button>
</div>
<form v-else @submit.prevent="handleSubmit">
<input v-model="email" type="email" placeholder="your@email.com" required />
<button type="submit" :disabled="loading">
{{ loading ? "Joining..." : "Join waitlist" }}
</button>
</form>
</template>
The Window interface declaration is optional but removes the Property 'OperatorStack' does not exist on type 'Window' TypeScript error everywhere you reference it. Put it in one shims-operatorstack.d.ts file at the project root instead of repeating it per component.
window.OperatorStack.ready is a promise that resolves once the SDK has fetched your project config. Await it at the top of handleSubmit (as above) rather than assuming the script has finished loading by the time someone clicks submit.
Route Changes Are Tracked Automatically, in History Mode
Vue Router's default createWebHistory() calls history.pushState on every navigation, the same mechanism a full page load would trigger via the browser's address bar. OperatorStack's embed script patches history.pushState globally and listens for popstate, then fires a page-view event 100ms after either one, debounced to avoid double-counting when a navigation triggers both. That gives a single-page Vue app per-route analytics without wiring up a router-specific tracking call.
<script setup lang="ts">
import { useRouter } from "vue-router";
// No extra code needed -- this navigation is already tracked
const router = useRouter();
function goToPricing() {
router.push("/pricing");
}
</script>
This only works with createWebHistory(). If your router uses createWebHashHistory() (URLs like /#/pricing), route changes update the hash instead of calling pushState, so OperatorStack will not see them and you will only get the initial page view. Switch to history mode, or fire window.OperatorStack.trackEvent("page_view", { path: to.path }) manually from a router.afterEach guard if hash mode is a hard requirement.
Common Mistakes
Putting the script tag inside a component instead of index.html. A script tag rendered in a Vue template only executes once, the first time that component mounts, and Vue's reactivity can skip re-inserting it on subsequent renders. Keep it in index.html where it loads exactly once, guaranteed.
Forgetting the TypeScript global declaration. Without it, every window.OperatorStack reference is a type error in <script setup lang="ts"> blocks. Add it once in a .d.ts file included by your tsconfig.json.
Assuming you need a composable for the default widget. The data-os-widget="waitlist" div works with zero Vue-specific code. Reach for the SDK and ref() only when you need a custom UI or want to read back referral_code for a share screen.
Frequently Asked Questions
Where does the OperatorStack script tag go in a Vue app?
In index.html, right before the closing tag. A plain client-side Vue app built with Vite has no server render step, so window is already available by the time your root component mounts. No onMounted guard is needed just to reference window.OperatorStack from a script tag in the same document.
Do I need an npm package to use OperatorStack in Vue?
No. OperatorStack is a script tag, not an npm package. It attaches window.OperatorStack once it loads, and you call its methods from your components exactly like any other browser global, including from the Composition API.
Does OperatorStack track page views across Vue Router routes?
Yes, automatically, in history mode. Vue Router's createWebHistory calls history.pushState on every navigation. OperatorStack patches pushState and listens for popstate, then fires a page-view event 100ms later, debounced so a route change never counts twice. Hash mode routers do not update pushState the same way, so if you use createWebHashHistory you will only see the initial page view.
Can I use TypeScript with the OperatorStack SDK in Vue?
Yes. Add a global Window interface declaration once in a shims-operatorstack.d.ts file so window.OperatorStack has types across every component and composable, or cast to any if you only reference it in one place.
How do I avoid a flash of empty space before the waitlist widget renders?
Await window.OperatorStack.ready before showing the form, or render a skeleton until it resolves. The ready promise resolves once the SDK has fetched your project config, which typically takes under a second.