Frameworks and libraries
Add a contact form to Astro
Astro's whole point is shipping less JavaScript, so a contact form that drags in a client-side framework misses the plot. The version below ships zero bytes of JS: a plain HTML form posting to your endpoint, with a `_next` field to send the visitor to a thank-you page. If you do want an inline success state, there is a small island for it, and an Astro Actions example if you are running with an SSR adapter.
Setup
Create a form, copy the endpoint
One URL, nothing to install into your Astro project.
Drop in the HTML
A plain <form> in a .astro file works on a fully static build. Add _next to control the redirect after submit.
Optionally enhance it
Add the island below if you want to stay on the page and show an inline confirmation.
Astro examples
Swap in your own endpoint and these run as-is.
Zero JavaScript. Works on a fully static build served from any CDN.
---
const ENDPOINT = "https://send.webforms.to/wft_your_key";
---
<form action={ENDPOINT} method="POST">
<input name="name" placeholder="Your name" required />
<input name="email" type="email" placeholder="you@example.com" required />
<textarea name="message" placeholder="Message" required></textarea>
<!-- where the browser goes after a successful submit -->
<input type="hidden" name="_next" value="https://example.com/thanks" />
<!-- honeypot -->
<input name="_gotcha" tabindex="-1" aria-hidden="true"
style="position:absolute;left:-9999px" />
<button type="submit">Send</button>
</form>Progressive enhancement with a small inline script - still no framework runtime.
<form id="contact" action="https://send.webforms.to/wft_your_key" method="POST">
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
<p id="result" role="status" hidden></p>
</form>
<script>
const form = document.getElementById("contact");
const result = document.getElementById("result");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const res = await fetch(form.action, {
method: "POST",
headers: { accept: "application/json" },
body: new FormData(form),
});
const data = await res.json();
result.hidden = false;
result.textContent = data.ok ? "Thanks, we got it." : data.message;
if (data.ok) form.reset();
});
</script>Astro Actions, for projects running an SSR adapter.
import { defineAction } from "astro:actions";
import { z } from "astro:schema";
export const server = {
contact: defineAction({
accept: "form",
input: z.object({
name: z.string(),
email: z.string().email(),
message: z.string(),
}),
handler: async (input) => {
const res = await fetch("https://send.webforms.to/wft_your_key", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
},
body: JSON.stringify(input),
});
if (!res.ok) throw new Error("Submission failed");
return { ok: true };
},
}),
};Worth knowing
- The static example is the one to reach for. It needs no adapter, no island and no hydration, which is the reason you picked Astro.
Astro form FAQ
Yes. A native HTML form POST needs no server at all. Add a _next field with your thank-you page URL and the browser is redirected there once the submission is accepted.
No. An adapter is only needed if you want to use Astro Actions or otherwise run server code. The static form works on GitHub Pages, Netlify, Vercel or any bucket.
None, in the first example. The enhanced version adds a few lines of inline script and no framework runtime.
Related guides
Ready to wire up your Astro form?
Free for 300 submissions a month. Endpoint in under a minute, no card required.
Start free