In Nuxt, create a ContactForm.vue component that submits FormData to your FormSubmit endpoint with fetch. You don't need a Nitro server route or an email module, and the form keeps working when you deploy a fully static site with nuxt generate.
1. Component
<!-- components/ContactForm.vue -->
<script setup lang="ts">
import { ref } from "vue";
const ENDPOINT = "https://formsubmit.app/f/YOUR_FORM_ID";
const status = ref("idle");
const error = ref("");
const renderedAt = Date.now();
async function onSubmit(e) {
status.value = "sending";
const data = new FormData(e.target);
data.set("_ts", String(renderedAt));
try {
const res = await fetch(ENDPOINT, { method: "POST", body: data, headers: { Accept: "application/json" } });
const json = await res.json();
if (!res.ok || !json.ok) throw new Error(json.error?.message ?? "Something went wrong");
status.value = "success";
e.target.reset();
} catch (err) {
error.value = err.message;
status.value = "error";
}
}
</script>
<template>
<p v-if="status === 'success'" class="text-green-700">Thanks! We'll be in touch soon.</p>
<form v-else class="mx-auto max-w-lg space-y-5" @submit.prevent="onSubmit">
<div class="space-y-1.5">
<label for="fs-name" class="block text-sm font-medium text-gray-900">Name</label>
<input id="fs-name" type="text" name="name" placeholder="Jane Doe" required class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20">
</div>
<div class="space-y-1.5">
<label for="fs-email" class="block text-sm font-medium text-gray-900">Email</label>
<input id="fs-email" type="email" name="email" placeholder="jane@example.com" required class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20">
</div>
<div class="space-y-1.5">
<label for="fs-message" class="block text-sm font-medium text-gray-900">Message</label>
<textarea id="fs-message" name="message" rows="5" placeholder="How can we help?" required class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"></textarea>
</div>
<!-- Honeypot: leave this hidden field empty to catch bots -->
<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none">
<p v-if="status === 'error'" class="text-sm text-red-600">{{ error }}</p>
<button type="submit" :disabled="status === 'sending'" class="inline-flex w-full items-center justify-center rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white hover:bg-gray-800 disabled:opacity-50">
{{ status === "sending" ? "Sending…" : "Send" }}
</button>
</form>
</template>Nuxt auto-imports components, so you can use <ContactForm /> anywhere.
2. Page
<script setup lang="ts">
useSeoMeta({ title: "Contact", description: "Get in touch with our team." });
</script>
<template>
<main class="mx-auto max-w-xl px-4 py-16">
<h1 class="mb-8 text-3xl font-semibold">Contact</h1>
<ContactForm />
</main>
</template>3. Optional: runtime config
export default defineNuxtConfig({
runtimeConfig: { public: { formsubmitUrl: "https://formsubmit.app/f/YOUR_FORM_ID" } },
});Then use useRuntimeConfig().public.formsubmitUrl in the component.
Why not a server route?
A Nitro route that sends email means managing an email provider, secrets, spam protection and storage — and it stops working on purely static hosting. FormSubmit handles all of that, including retries for integrations and a searchable inbox.
Before launch
Add your domain to the allowlist and consider an autoresponder.