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:
# .env
VITE_WAITLIST_KEY=your_waitlist_key_here
# For Nuxt:
# NUXT_PUBLIC_WAITLIST_KEY=your_waitlist_key_here
Create 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
<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>
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. 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
Yes! Works with both Vue 3 (Composition API and Options API) and Vue 2 (Options API). Vue 3 is recommended for new projects with better TypeScript support and performance.
Both! We provide examples for both styles. Composition API with <script setup> is more modern and concise. Options API is familiar and still fully supported in Vue 3.
Absolutely! Full Nuxt 3 support with SSR, SSG, and auto-imports. Works with Nuxt's file-based routing, server API routes, and useRuntimeConfig for env variables.
Yes! Vue 3 + Vite is the recommended setup. Lightning-fast HMR, optimized builds, and great DX. All our examples use Vite conventions (import.meta.env, etc.).
Create a Pinia store for waitlist state management. Use the store across your entire app for consistent state. Perfect for tracking signup status, showing success messages, or managing multiple waitlists.
Yes! Quasar is built on Vue, so all integration methods work. Use Quasar components for beautiful mobile-first forms. Perfect for building cross-platform apps with waitlists.
Absolutely! Full TypeScript support with proper types for composables, props, and events. Use defineProps<T>() and defineEmits<T>() for type-safe components.
Use VeeValidate for robust validation, or build custom validation with computed properties and watchers. Vue's reactivity makes validation elegant and straightforward.
Yes! Works perfectly with Vue Router. Add waitlist forms to any route, create dedicated /launch pages, or use route meta for conditional displays. All router integrations work out of the box.
Yes! Both methods work with UI frameworks. For custom forms, use their form components and connect to Waitlister. For embed, place it anywhere in your component tree.
Use @vue/test-utils for unit testing. Mock the fetch calls in your tests. Test form submission, validation, error states, and success flows. All standard Vue testing practices apply.
Yes! The embed widget works with SSR. For custom forms, use client-only components or useClientOnly() if needed. Server API routes work great for backend logic in Nuxt.
Get started for free
Start collecting sign ups for your
product launch in minutes — no coding required.