splitforms.com
VUE · CONTACT FORM

Contact form for Vue.js websites

Composition API, Options API, Nuxt, Vite — every Vue setup works with one tiny single-file component. Submit a form, get email notifications, dashboard analytics, and webhooks. No backend, no SDK, no $30/mo plan.

1,000 free submissions every month.·No credit card.
contact.vuevue33 lines
01<script setup>
02import { ref } from "vue";
03
04const status = ref("idle");
05
06async function onSubmit(e) {
07 status.value = "loading";
08 const formData = new FormData(e.target);
09 formData.append("access_key", "YOUR_ACCESS_KEY");
10
11 const res = await fetch("https://splitforms.com/api/submit", {
12 method: "POST",
13 body: formData,
14 });
15 const data = await res.json();
16 status.value = data.success ? "ok" : "err";
17 if (data.success) e.target.reset();
18}
19</script>
20
21<template>
22 <form @submit.prevent="onSubmit">
23 <input name="name" placeholder="Name" required />
24 <input name="email" type="email" placeholder="Email" required />
25 <textarea name="message" placeholder="Message" required />
26 <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />
27 <button :disabled="status === 'loading'">
28 {{ status === 'loading' ? 'Sending…' : 'Send' }}
29 </button>
30 <p v-if="status === 'ok'">Thanks!</p>
31 <p v-if="status === 'err'">Error.</p>
32 </form>
33</template>
1,000
submissions / mo, free
14ms
median latency, edge
0
lines of backend code
17+
frameworks supported
✶ Live preview

What your Vue contact form actually looks like.

Drop-in form backend with spam filtering, signed webhooks, and a real submissions dashboard. The same code in this preview is what you copy into your Vue project — no SDK, no plugin, no PHP.

  • 1,000 submissions per month, free forever
  • Honeypot + AI spam classifier on every plan
  • Signed webhooks to Slack, Discord, your server
Vue contact form on Splitforms — drop-in form backend with spam filtering and webhooks
§ 01Setup3 steps · 60 seconds · zero config

Ship a Vue contact form without a backend.

No SDK, no PHP, no plugin. Your form posts standard FormData to one URL — submissions land in your inbox.

STEP 01GENERATE

Get your free access key

Verify your email and your access key is generated instantly. Free for 1,000 submissions per month, forever.

Create your form

By signing up, you agree to our terms and privacy policy.

STEP 02EMBED

Drop in the Vue code

Copy the Vue snippet on the right and paste it into your project. Replace YOUR_ACCESS_KEY with the key from step 1.

snippetvue
<script setup>
…
STEP 03RECEIVE

Submissions land in your inbox

Hits your dashboard and email in seconds. Forward to Slack, Discord, Sheets, Notion, or any signed webhook URL.

inbox · 1 newjust now
FROM contact@yoursite.com
New Vue form submission
Maya Iyer maya@studio71.co
Loved the new pricing page — quick question about the 4-year plan. Are usage limits per project or account-wide?
§ 02Live demosandboxed · no key required · no submission sent

Try it now — no signup, no key.

This is a styled HTML preview of what your Vue form will look like. Submitting opens a confirmation, no real request is sent.

preview · vuelocalhost:3000
✦ what just happened

Your Vue form posts FormData to /api/submit. Splitforms validates the access key, runs the spam classifier, and forwards the parsed submission to your inbox plus the dashboard.

  • 14ms median round-trip from the edge.
  • Honeypot + classifier, no CAPTCHA.
  • Per-domain key locking out of the box.
REQUEST · POST /api/submit
{
  "access_key": "sk_live_4f9a_••••",
  "name":       "Maya Iyer",
  "email":      "maya@studio71.co",
  "message":    "…"
}
← 200 OK · { "success": true } · 14ms
§ 03Best practices5 rules · production-tested

How to ship this without regrets.

Five rules that make the difference between a form that works in the demo and a form that survives launch traffic.

  1. 01

    Use `<script setup>` syntax for the contact form. Half the boilerplate, full reactivity, no `defineExpose` needed.

  2. 02

    Type your status ref: `const status = ref<'idle' | 'loading' | 'ok' | 'err'>('idle')`. TypeScript catches typos in your template's v-if blocks before they ship.

  3. 03

    Set the access key via `import.meta.env.VITE_SPLITFORMS_KEY` (Vite) or `useRuntimeConfig().public.splitformsKey` (Nuxt) — never hardcode the literal string in your component file.

  4. 04

    After success, call `e.target.reset()` then nextTick() before showing the success message — gives Vue a frame to update v-models so the success state isn't visually fighting the form clear.

  5. 05

    Add `:aria-busy="status === 'loading'"` on the form for screen readers — Vue evaluates this on every reactive update for free.

§ 04Common gotchas in Vue6 edge cases worth knowing

What bites people who skip the docs.

Worth a 60-second skim before you ship to production. Each one has caused a Vue support ticket at least once.

⚠ gotcha

ref() doesn't auto-unwrap inside a template handler

If you write status === 'loading' inside <script setup> it works because Vue auto-unwraps refs in templates — but inside a function you need status.value. Mixing the two is the #1 cause of "why isn't my button disabling?" bugs in Vue contact forms.

⚠ gotcha

v-model and FormData read different sources

If you bind inputs with v-model="name" to a ref and then build new FormData(e.target), FormData reads the DOM — which Vue keeps in sync — so it works. But if you bind a computed value and the input doesn't have a name attribute, FormData drops it silently. Always set name="…" on every input.

⚠ gotcha

@submit fires before .prevent unless you spell it right

Vue's event modifier syntax is @submit.prevent="onSubmit". People often write @submit="onSubmit" and then forget to call e.preventDefault() inside the handler — the page reloads and the fetch is cancelled mid-flight.

⚠ gotcha

Vite env vars need the VITE_ prefix or they're undefined at build

Reading import.meta.env.SPLITFORMS_KEY returns undefined unless you rename it to VITE_SPLITFORMS_KEY (Vite) or expose it via runtimeConfig (Nuxt). Vite intentionally hides anything without the prefix to prevent leaking server secrets.

⚠ gotcha

Vue 2 needs vue-fetch or axios — there's no built-in fetch wrapper

Modern browsers all support fetch, but if you support IE11 (Vue 2 territory), you need a polyfill or axios. Splitforms only requires a POST with FormData — any client lib works.

⚠ gotcha

reactive() unwraps inputs but breaks if you destructure the form object

If you wrap your form fields in const form = reactive({ name: '', email: '', message: '' }) and then do const { name, email } = form to shorten template references, you lose reactivity — the destructured locals are plain primitives, not refs. The submit handler reads stale values and you get empty fields in the splitforms inbox. Use toRefs(form) when destructuring, or skip destructuring and reference form.name directly. Same trap exists for props passed from a parent.

§ 04bNative Vue forms…and where they break down

How Vue handles forms without splitforms.

The shape of the problem before splitforms enters the picture — and the gap it fills for Vue specifically.

Vue ships nothing form-related beyond v-model for two-way binding. To deliver a submission anywhere, you write the same backend stack as React: an Express/Nitro/Hono route, an email provider, a database, spam filtering. Nuxt narrows the gap with /server/api routes, but you're still operating the route. Pinia/Vuex don't help — they're for client state, not network. The Vue ecosystem has FormKit and VeeValidate for validation UX, but neither delivers submissions. Splitforms slots in as the missing endpoint: any Vue 2 / Vue 3 / Nuxt setup posts a FormData to one URL, gets { success: true } back, done.

§ 04cAlternative integration patterns2 ways to wire it

Two ways to ship splitforms on Vue.

Pick the pattern that matches your constraints — JS budget, key-exposure tolerance, server-side opacity. Both produce the same result.

PATTERN A

Pattern A — `<script setup>` with status ref

Modern Vue 3 SFC, half the boilerplate of Options API, full reactivity. status is a single ref<'idle' | 'loading' | 'ok' | 'err'>.

pattern-a.vuevue17 lines
01<script setup>
02import { ref } from "vue";
03const status = ref("idle");
04async function onSubmit(e) {
05 status.value = "loading";
06 const fd = new FormData(e.target);
07 fd.append("access_key", import.meta.env.VITE_SPLITFORMS_KEY);
08 const r = await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
09 status.value = (await r.json()).success ? "ok" : "err";
10}
11</script>
12<template>
13 <form @submit.prevent="onSubmit">
14 <input name="email" type="email" required />
15 <button :disabled="status === 'loading'">Send</button>
16 </form>
17</template>
PATTERN B

Pattern B — composable for reuse across forms

Wrap the splitforms POST in a useSplitforms() composable. Multiple forms (contact, newsletter, demo) share one implementation. Test once, ship everywhere.

pattern-b.vuevue14 lines
01// composables/useSplitforms.js
02import { ref } from "vue";
03export function useSplitforms() {
04 const status = ref("idle");
05 async function submit(formEl, formName) {
06 status.value = "loading";
07 const fd = new FormData(formEl);
08 fd.append("access_key", import.meta.env.VITE_SPLITFORMS_KEY);
09 fd.append("form-name", formName);
10 const r = await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
11 status.value = (await r.json()).success ? "ok" : "err";
12 }
13 return { status, submit };
14}
§ 04dDeployment notes for Vuehosting · env vars · CSP

Shipping Vue + splitforms to production.

Host-specific gotchas, env-var conventions, and the boring-but-load-bearing details for putting this on the public internet.

Vue + Vite produces a static bundle that deploys anywhere. For Nuxt, every Nitro preset (Vercel, Netlify, Node, Cloudflare, static, AWS Lambda) works identically because the form posts client-side. On Cloudflare Pages with the Nuxt Cloudflare preset, avoid proxying through /server/api — the fetch round-trip eats the 10ms CPU budget on the free tier; post directly to splitforms. Vite-only (non-Nuxt) projects must use VITE_SPLITFORMS_KEY or env vars are silently undefined client-side. Nuxt uses runtimeConfig.public.splitformsKey. Both inline the value into the bundle, so domain-lock the key in the splitforms dashboard.

§ 05Comparisonvs native vue

splitforms vs native vue.

What you get for free vs what you build, pay for, or do without.

FeatureNative Vuesplitforms
Setup timeExpress/Hono backend + email + spam filter (1 day)60 seconds, one component
Component codeForm + axios + error states + retry logic20 lines, one fetch call
Spam filteringDIY honeypot or hCaptcha integrationHoneypot + classifier built-in
Submission storagePostgres + Prisma + authDashboard included free
Vue 2 + Vue 3 supportTwo codebasesSame component, both versions
Cost (1k subs/mo)Server $5 + email $10 + storage$0 (within free tier)
§ 06Alternative patternvue · 37 lines
ALTERNATIVE

Options API variant for Vue 2 / Nuxt 2 / legacy code

If you're maintaining a Vue 2 codebase or prefer the Options API, the same flow works without `<script setup>` — just move the logic into `data()` and `methods`.

alternative.vuevue37 lines
01<template>
02 <form @submit.prevent="onSubmit">
03 <input v-model="form.name" name="name" required />
04 <input v-model="form.email" name="email" type="email" required />
05 <textarea v-model="form.message" name="message" required />
06 <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" />
07 <button :disabled="status === 'loading'" type="submit">
08 {{ status === 'loading' ? 'Sending…' : 'Send' }}
09 </button>
10 <p v-if="status === 'ok'">Thanks!</p>
11 <p v-if="status === 'err'">Error — please retry.</p>
12 </form>
13</template>
14
15<script>
16export default {
17 data() {
18 return {
19 status: "idle",
20 form: { name: "", email: "", message: "" },
21 };
22 },
23 methods: {
24 async onSubmit() {
25 this.status = "loading";
26 const body = new FormData();
27 Object.entries(this.form).forEach(([k, v]) => body.append(k, v));
28 body.append("access_key", "YOUR_ACCESS_KEY");
29
30 const res = await fetch("https://splitforms.com/api/submit", { method: "POST", body });
31 const data = await res.json();
32 this.status = data.success ? "ok" : "err";
33 if (data.success) this.form = { name: "", email: "", message: "" };
34 },
35 },
36};
37</script>
§ 07Questions6 answered

Things developers ask before they integrate.

Direct answers, no marketing fluff. Missing one? Email hello@splitforms.com.

01How do I add a contact form to Vue?
Drop the <script setup> single-file component above into any .vue file in your project, replace YOUR_ACCESS_KEY, and import it where you need it. No router config, no backend, no Vuex/Pinia required.
02Does splitforms work with Vue SSR / SSG?
Yes. The form component is client-only (it uses fetch), but it hydrates cleanly into any SSR-rendered page. For Nuxt SSG, see the dedicated /forms/nuxt page — same backend, Nuxt-specific runtimeConfig pattern.
03How do I handle form errors in Vue?
Use a status ref with four states: idle, loading, ok, err. Show inline messages with v-if for ok/err. The fetch call returns { success: boolean, message?: string } — render data.message for the user when success is false.
04Can I use splitforms with Vue 3 server functions / Nuxt server routes?
Yes. If you'd rather hide the access key, post from a Nuxt /server/api/contact.post.ts route or a Vue 3 server function. The Nuxt page covers this pattern in detail.
05How do I customize the success / redirect behavior?
Two options. (1) Stay on-page and show a Vue-rendered success message (the default in our snippet). (2) Pass <input type="hidden" name="redirect" value="/thanks" /> and use a non-AJAX form submit — the splitforms response 302s to that URL.
06Does this work with Vuetify, Quasar, PrimeVue, or Element Plus?
Yes. Use the UI library's form/input components for the markup, but make sure each input has a name attribute (some component libraries omit it by default). FormData reads from name attributes, not v-model bindings.
✻ ✻ ✻

Ship your Vue contact form in 60 seconds.

1,000 free submissions per month. No credit card. Lock the access key to your domains, paste the snippet, watch submissions land in your inbox.

Get free access key →Read the docs
v0.1 · founders pricing locked in · early access open