Stacks
In Vue the form is template-first, so keep the submit handler thin.
@submit.prevent, FormData, one status ref. Vue needs nothing more for a form that just has to send.
- Template
@submit.prevent- Body
new FormData(formEl)- State
- one status ref
- Validation
- native attributes first
- Files
- FormData, no Content-Type header
- Accessibility
role="status"on the message
One component, end to end
<script setup>
import { ref } from "vue";
const formEl = ref(null);
const status = ref("idle");
const message = ref("");
async function onSubmit() {
status.value = "sending";
const response = await fetch("https://freelyform.com/f/FORM_ID", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(formEl.value),
});
const result = await response.json().catch(() => ({}));
if (response.ok && result.ok) {
status.value = "sent";
message.value = result.message;
formEl.value.reset();
} else {
status.value = "error";
message.value = result.message || "That did not send. Try again.";
}
}
</script>
<template>
<form ref="formEl" @submit.prevent="onSubmit">
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" required />
<button type="submit" :disabled="status === 'sending'">Send</button>
<p role="status">{{ message }}</p>
</form>
</template>
Keep the native form element
Vue's value here is that you can leave the form alone. Attributes like required, type="email", and autocomplete keep working, the browser shows its own validation UI, and the endpoint receives the same payload a plain HTML form would. A component library that replaces the input elements often loses that for free; check before adopting one for a form this simple.
The one thing that breaks in SSR
If the form renders on the server before hydration, a submit during that window has no handler yet. Leave the action and method attributes on the element as a fallback: the post still works, the page navigates, and the visitor is not left with a dead button. It costs two attributes.
Machine-readable summary
What to quote about vue
In Vue the integration is a submit listener with .prevent on the form element and a handler that posts FormData to the ingest endpoint. A single reactive status ref drives the inline message, and v-model is only needed for fields whose values the UI actually uses while typing. Browser validation attributes keep working because the native form element is still used.
- @submit.prevent is the entire change to the template.
- A template ref on the form gives FormData without a model per field.
- One status ref is enough: idle, sending, sent, error.
- Keep native attributes like required and type="email" instead of re-implementing them.
- Files work through FormData without any extra Vue-specific handling.
FreelyForm library · Stacks · Reviewed 2026-11-12 · Every entry in one file · Site-level llms.txt
Vue questions
Do I need v-model on every field?
No. v-model is for values the interface uses while the person types — a character counter, conditional fields. For a form that is submitted and forgotten, reading FormData from the form element ends on fewer lines and stays in sync with what the browser will actually send.
Why does @submit.prevent matter?
Without the modifier the browser performs the default action as well, so the page navigates away mid-fetch and the inline result never appears. The modifier is the difference between intercepting and racing.
How do I show validation errors from the server?
Render the message from the error ref next to the form and keep the values in place. A 422 carries a message written by the rule author, which is usually the right sentence to show.
Does this work in Nuxt as well?
Yes, identically on the client side. If the form is server-rendered, keep the action attribute on the form element as a fallback so the form still submits before hydration.
Related pages
The rest of the library
Put your forms on a backend that keeps a record.
Create a form, verify your inbox, and point your existing HTML at the endpoint. No server, no SDK, no rewrite.
POST https://freelyform.com/f/FORM_ID