Vue
Add waitlists to your Vue apps the progressive way
Simple, reactive, and developer-friendly. Works with Vue 3 Composition API, Options API, and Nuxt. Drop in a component or build custom forms with Vue's powerful reactivity. Vite-ready and production-tested.
Trusted by 2,000+
businesses & entrepreneurs
.png?alt=media&token=939637fa-d391-4d15-85ea-7005e07d08eb)







.png?alt=media&token=264537c9-b2e0-44e7-9d78-3b558d4e10c2)





.png?alt=media&token=939637fa-d391-4d15-85ea-7005e07d08eb)







.png?alt=media&token=264537c9-b2e0-44e7-9d78-3b558d4e10c2)





“Waitlister provides a one-of-a-kind builder platform to design your own waitlist page that actually stands out from competitors. Once the page is ready and deployed, we never worry about uptime or data reliability.”
What you can build
Popular ways Vue users implement waitlists
SaaS Dashboard Launches
Build waitlist flows into your Vue-powered admin dashboards and SaaS applications with reactive state.
Marketing Sites with Nuxt
Server-rendered landing pages with Nuxt 3 for SEO-perfect pre-launch campaigns.
Progressive Enhancement
Add Vue incrementally to existing sites with waitlist components that just work.
Mobile Apps with Quasar
Build cross-platform mobile apps with waitlist functionality using Quasar Framework.
Admin Panels & Internal Tools
Add feature request waitlists or beta program signups to internal Vue admin panels.
Component Libraries
Build reusable waitlist components with Vue's powerful composition and props system.
Why Waitlister for Vue?
Built to work seamlessly with Vue's capabilities
Reactive by Default
Vue's reactivity system makes form state management effortless. Automatic updates, computed properties, and watchers work seamlessly with waitlist forms.
Composition API Ready
Modern composables pattern with <script setup>. Reusable waitlist logic with useWaitlist() composable. Works with Options API too for backward compatibility.
Single-File Components
Everything in one .vue file - template, logic, and styles. Better DX than JSX for many developers. Scoped styles prevent CSS conflicts.
Vite-Powered Speed
Lightning-fast HMR with Vite. Instant feedback during development. Optimized production builds with automatic code splitting.
Nuxt 3 Compatible
Full support for Nuxt 3 with SSR, SSG, and auto-imports. Perfect for SEO-optimized landing pages and marketing sites.
State Management Built-in
Works beautifully with Pinia, Vuex, or Vue's built-in reactivity. Share waitlist state across your entire app effortlessly.
Which integration is
right for you?
Compare both methods to find the best fit for your Vue project
| Feature | Form Action | Embeddable Widget |
|---|---|---|
| Setup Time | ~10 minutes | ~5 minutes |
| Reactivity | Full Vue reactivity | Internal |
| Composition API | Full support | N/A |
| Options API | Full support | N/A |
| TypeScript | Full type safety | Basic |
| Customization | Complete control | Dashboard only |
| Best For | Production apps | Quick launches |
Choose Form Action if...
- You want full control with Vue reactivity
- You're using Composition API or Options API
- You need custom validation or complex logic
- You want to integrate with Pinia or Vuex
- You're building a production Vue application
- You need type-safe forms with TypeScript
Choose Embeddable Widget if...
- You want the fastest possible setup
- You're building a simple landing page
- You don't need deep Vue integration
- You prefer managing form design externally
- You're prototyping or testing an idea
How to integrate
Follow these Vue-specific instructions
Set up environment variables
Create a .env file in your project root. Get your waitlist key from your Waitlister dashboard:
# .env
VITE_WAITLIST_KEY=your_waitlist_key_here
# For Nuxt:
# NUXT_PUBLIC_WAITLIST_KEY=your_waitlist_key_hereCreate waitlist composable (Composition API)
Build a reusable composable for waitlist logic:
// composables/useWaitlist.ts
import { ref } from 'vue';
export interface WaitlistData {
email: string;
name?: string;
}
export const useWaitlist = () => {
const loading = ref(false);
const success = ref(false);
const error = ref<string | null>(null);
const submitToWaitlist = async (data: WaitlistData) => {
loading.value = true;
error.value = null;
try {
const response = await fetch(
`https://waitlister.me/s/${import.meta.env.VITE_WAITLIST_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(data as any)
}
);
if (!response.ok) {
throw new Error('Submission failed');
}
success.value = true;
return { success: true };
} catch (err) {
error.value = 'Failed to join waitlist. Please try again.';
return { success: false, error: error.value };
} finally {
loading.value = false;
}
};
const reset = () => {
loading.value = false;
success.value = false;
error.value = null;
};
return {
loading,
success,
error,
submitToWaitlist,
reset
};
};Create waitlist form component
Build a reactive form component using the composable:
<!-- components/WaitlistForm.vue -->
<template>
<div v-if="success" class="success-message">
<h3>You're on the list! 🎉</h3>
<p>We'll notify you when we launch.</p>
</div>
<form v-else @submit.prevent="handleSubmit" class="waitlist-form">
<input
v-model="formData.email"
type="email"
placeholder="Your email"
required
/>
<input
v-model="formData.name"
type="text"
placeholder="Your name (optional)"
/>
<button type="submit" :disabled="loading">
{{ loading ? 'Joining...' : 'Join Waitlist' }}
</button>
<div v-if="error" class="error-message">{{ error }}</div>
</form>
</template>
<script setup>
import { reactive } from 'vue';
import { useWaitlist } from '@/composables/useWaitlist';
const { loading, success, error, submitToWaitlist } = useWaitlist();
const formData = reactive({
email: '',
name: ''
});
const handleSubmit = async () => {
await submitToWaitlist(formData);
if (success.value) {
formData.email = '';
formData.name = '';
}
};
</script>
<style scoped>
.waitlist-form {
max-width: 500px;
margin: 0 auto;
}
.waitlist-form input {
width: 100%;
padding: 12px 16px;
margin-bottom: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.waitlist-form button {
width: 100%;
padding: 14px 24px;
background: #42b883;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
.waitlist-form button:hover:not(:disabled) {
background: #33a06f;
}
.waitlist-form button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-message {
margin-top: 12px;
padding: 12px;
background: #fee;
color: #c33;
border-radius: 4px;
}
.success-message {
text-align: center;
padding: 2rem;
}
</style>Options API version
If you prefer Options API, here's the equivalent:
<!-- components/WaitlistForm.vue -->
<template>
<div v-if="success" class="success-message">
<h3>You're on the list! 🎉</h3>
<p>We'll notify you when we launch.</p>
</div>
<form v-else @submit.prevent="handleSubmit" class="waitlist-form">
<input
v-model="email"
type="email"
placeholder="Your email"
required
/>
<input
v-model="name"
type="text"
placeholder="Your name (optional)"
/>
<button type="submit" :disabled="loading">
{{ loading ? 'Joining...' : 'Join Waitlist' }}
</button>
<div v-if="error" class="error-message">{{ error }}</div>
</form>
</template>
<script>
export default {
name: 'WaitlistForm',
data() {
return {
email: '',
name: '',
loading: false,
success: false,
error: null
};
},
methods: {
async handleSubmit() {
this.loading = true;
this.error = null;
try {
const response = await fetch(
`https://waitlister.me/s/${import.meta.env.VITE_WAITLIST_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
email: this.email,
name: this.name
})
}
);
if (!response.ok) throw new Error('Submission failed');
this.success = true;
this.email = '';
this.name = '';
} catch (err) {
this.error = 'Failed to join waitlist. Please try again.';
} finally {
this.loading = false;
}
}
}
};
</script>Integrate with Pinia store
For global state management, use Pinia:
// stores/waitlist.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useWaitlistStore = defineStore('waitlist', () => {
const signups = ref<string[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
async function submit(email: string, name?: string) {
loading.value = true;
error.value = null;
try {
const response = await fetch(
`https://waitlister.me/s/${import.meta.env.VITE_WAITLIST_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ email, name: name || '' })
}
);
if (!response.ok) throw new Error('Failed');
signups.value.push(email);
return true;
} catch (err) {
error.value = 'Failed to join waitlist';
return false;
} finally {
loading.value = false;
}
}
return { signups, loading, error, submit };
});
// Use in component:
// import { useWaitlistStore } from '@/stores/waitlist';
// const waitlist = useWaitlistStore();
// await waitlist.submit(email, name);Nuxt 3 server API route
For Nuxt 3, create a server route for additional backend logic:
// server/api/waitlist.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { email, name } = body;
// Validation
if (!email || !email.includes('@')) {
throw createError({
statusCode: 400,
message: 'Invalid email'
});
}
// Forward to Waitlister
try {
const response = await $fetch(
`https://waitlister.me/s/${process.env.WAITLIST_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ email, name })
}
);
return { success: true, message: 'Joined waitlist' };
} catch (error) {
throw createError({
statusCode: 500,
message: 'Failed to join waitlist'
});
}
});
// Call from component:
// const { data, error } = await useFetch('/api/waitlist', {
// method: 'POST',
// body: { email, name }
// });Add form validation with VeeValidate
For production-grade validation, integrate VeeValidate:
npm install vee-validate yup
// npm install vee-validate yup
<template>
<form @submit="onSubmit" class="waitlist-form">
<input
v-model="email"
type="email"
placeholder="Your email"
:class="{ error: errors.email }"
/>
<span v-if="errors.email" class="error">{{ errors.email }}</span>
<input
v-model="name"
type="text"
placeholder="Your name"
/>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? 'Joining...' : 'Join Waitlist' }}
</button>
</form>
</template>
<script setup>
import { useForm } from 'vee-validate';
import * as yup from 'yup';
import { useWaitlist } from '@/composables/useWaitlist';
const schema = yup.object({
email: yup.string().required().email(),
name: yup.string()
});
const { errors, handleSubmit, defineField, isSubmitting } = useForm({
validationSchema: schema
});
const [email] = defineField('email');
const [name] = defineField('name');
const { submitToWaitlist } = useWaitlist();
const onSubmit = handleSubmit(async (values) => {
await submitToWaitlist(values);
});
</script>Track and optimize
Monitor your waitlist performance with built-in analytics. Export subscriber data via subscriber management for deeper analysis.
Need more details?
Check out our complete form action endpoint documentation.
View full documentationCommon issues & solutions
Quick fixes for Vue-specific problems
Ensure script is in public/index.html before </body> tag. Vite serves public folder as-is. Check browser Network tab to verify script loads.
For Nuxt, add script to nuxt.config.ts app.head.script array.
Vite requires VITE_ prefix for client-accessible variables. Nuxt uses NUXT_PUBLIC_ prefix.
Restart dev server after adding new variables. Check .env is in project root and in .gitignore.
Make sure you're using reactive data (ref, reactive, computed). For Options API, check data properties are defined correctly.
Vue 3 reactivity requires proper use of ref() or reactive().
Verify the div has exact class name "waitlister-form". Check browser console for errors.
Ensure script loaded before component mounted. The widget needs the script to be present on page load.
Add your domain to Waitlister's whitelisted domains under Configure. Include localhost:5173 (or your Vite port) for development, localhost:3000 for Nuxt.
Check browser console for specific blocked domain.
Ensure proper type definitions for composables. Use TypeScript's satisfies operator or explicit return types.
Check tsconfig.json includes your composables directory.
Nuxt auto-imports components from components/ directory. Ensure file is in the right place.
Restart Nuxt dev server. Check .nuxt/types for generated types.
All reactive data must be defined in data() function. Don't assign properties outside data().
Use this.$set for adding properties dynamically in Vue 2 (not needed in Vue 3).
Pinia stores are reactive but not persistent by default. Install pinia-plugin-persistedstate for localStorage persistence, or manually save to localStorage in your submit method.
Common questions
About Vue integration
<script setup> is more modern and concise. Options API is familiar and still fully supported in Vue 3.defineProps<T>() and defineEmits<T>() for type-safe components.Get started with Waitlister
Integrate Waitlister with Vue to get the most out of your waitlist
