Astro contact forms, fully prerendered.
Astro ships zero JavaScript by default, and your contact form can too. Point a plain HTML form at one endpoint from any .astro component and every submission lands in your inbox and your dashboard — the site stays static, and deploys anywhere static files do.
A working form in one .astro component.
Start with the zero-JavaScript component — it's the whole integration. Add the fetch tab when you want an inline status message, and the pages tab shows the routing that completes the loop.
---
// src/components/ContactForm.astro
// Renders to plain HTML at build time — zero client JavaScript and
// nothing to hydrate. Works on a fully prerendered page.
---
<form action="https://api.formwire.com/submit" method="POST">
<input type="hidden" name="access_key" value="YOUR-ACCESS-KEY" />
<!-- A static page can't redirect after a POST, so FormWire does it.
Point _redirect at the absolute URL of your thank-you route. -->
<input type="hidden" name="_redirect" value="https://yoursite.com/thanks/" />
<label>
Name
<input type="text" name="name" required />
</label>
<label>
Email
<input type="email" name="email" required />
</label>
<label>
Message
<textarea name="message" required></textarea>
</label>
<button type="submit">Send</button>
</form> Swap YOUR-ACCESS-KEY for your form's key. The full field and response reference lives in the docs at /docs/.
The four things that trip up Astro forms.
Each one costs an afternoon the first time. The snippets above already account for all four.
View transitions kill one-shot scripts
With <ClientRouter /> a navigation swaps the DOM without a full page load, so a handler wired on DOMContentLoaded works exactly once — the form goes dead the moment a visitor arrives via a transition. Re-wire inside astro:page-load, which fires after every swap, and guard against double-binding.
Astro rewrites your <script> tags
Scripts in .astro components are processed, bundled, hoisted to the head as modules, and deduplicated — each runs once per page even if the component renders twice. When a script must stay exactly where you wrote it, or you're passing values with define:vars, mark it is:inline and Astro leaves it alone.
Skip the adapter — stay static
Astro Actions and API routes exist for servers you run, and a contact form doesn't need one: the browser POSTs to FormWire directly. Keep the build fully prerendered — no adapter, no output config change — and keep deploying plain files to any static host.
The redirect happens off-site
There's no server-side handler to send visitors to a thank-you page, so FormWire issues the redirect for you: set the hidden _redirect field to the absolute URL of a thanks route. Relative paths won't survive the round trip through the endpoint — spell out the full URL.
From nothing to a working form.
Grab your access key
Create a form in the dashboard and copy its public key. It's a safe-to-ship alias for your verified email, so it goes straight into your component and your repo.
Drop ContactForm.astro into src/components
Paste a recipe from above, swap in your key, and render <ContactForm /> in any page, layout or MDX file. The markup and styling stay entirely yours.
Submissions land in your inbox
Every POST arrives as a formatted email with Reply-To set to the visitor, and as a searchable record in your dashboard.
Built for a static-first framework.
One recipe for every island
The plain component covers most sites, and the same endpoint accepts a POST from any island — React, Svelte, Vue or Solid components inside your Astro pages submit the same way.
CORS preflight handled
The endpoint answers the OPTIONS preflight and returns readable JSON, so the fetch upgrade works from any domain — localhost:4321 included — with nothing to proxy.
Spam filtered server-side
A honeypot and optional captcha run before delivery, so the zero-JavaScript form stays zero-JavaScript and bots still don't reach your inbox.
Every submission stored
Beyond the email, each POST becomes a searchable record with CSV export and a JSON read API — useful when the form feeds more than one pair of eyes.
Astro form FAQ
The questions Astro developers actually ask.
No. Actions and API routes are for logic that runs on your own server, and the form doesn't have any — the browser POSTs straight to FormWire. A fully prerendered site handles submissions fine, and if you already render on demand for other reasons, the same snippets work unchanged.
Almost always view transitions. With <ClientRouter /> the page content is swapped in without a full load, so a script that wired the form on DOMContentLoaded never runs again. Attach your setup to the astro:page-load event, as in the fetch tab above, and the form is re-wired after every navigation.
Yes. Import the component at the top of an .mdx file and render <ContactForm /> inline, or place it once in the layout that wraps your content collection pages. It compiles to the same static HTML either way.
Yes. It's a public alias for your verified email, not a secret. Delivery only ever reaches recipients you've verified, so the most anyone can do with the key from your built HTML is send a submission that lands in your own inbox.
JSON for a standard form: stringify the fields with the access_key included and set Content-Type to application/json. Switch to FormData when a file input is involved — post the FormData object directly and leave the Content-Type header off so the browser sets the multipart boundary. Both hit the same endpoint.
Start on the free tier: 250 submissions a month, unlimited forms, and the endpoint and code are identical on every plan. Paid tiers add volume, file uploads and integrations like Slack.
See pricing →Building with another framework?
See all frameworksShip your Astro form today.
Grab an access key, paste a component, and route submissions to your inbox — or on to Slack, Sheets and Zapier.
Free tier forever. No credit card required.