# An Astro contact form that works on a static build

Send an Astro contact form to your email from a fully static site, with no adapter, Astro Actions or email API. Zero JavaScript by default, plus an optional script to submit in place.

Updated Oct 29, 2026.

Astro builds static HTML by default, and static HTML has nowhere to send a form. Astro Actions and API endpoints in `src/pages/api` both fix that by adding a server: an adapter, on-demand rendering, and an email provider with its key.

A Nisuform endpoint as the form's `action` keeps the site static. The browser posts the form to the endpoint, which filters spam, stores the submission and emails you. You keep `output: 'static'` and can host the `dist` folder anywhere.

## The form

```astro title="src/components/ContactForm.astro"
---
const endpoint = 'https://api.nisuform.com/s/YOUR_FORM_KEY'
---

<form id="contact-form" action={endpoint} method="POST">
  <label>
    Name
    <input type="text" name="name" autocomplete="name" required />
  </label>
  <label>
    Email
    <input type="email" name="email" autocomplete="email" required />
  </label>
  <label>
    Message
    <textarea name="message" rows="5" required></textarea>
  </label>
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display: none" />
  <button type="submit">Send</button>
  <p role="status" aria-live="polite"></p>
</form>
```

That's a complete form with zero JavaScript. On submit, the browser shows the Nisuform thank-you page with a link back to your site. On Pro you can send visitors to your own thank-you page with a [redirect](/docs/forms/after-submit).

[Create a form](https://app.nisuform.com) to get the key for `endpoint`. Its **Setup** tab also has an Astro version with your own field names under **Code**.

## Submitting without a reload

To keep visitors on the page, add this script to the same component. Astro bundles it and loads it once per page. If it never runs, the form still posts the normal way.

```astro title="src/components/ContactForm.astro"
<script>
  const form = document.querySelector<HTMLFormElement>('#contact-form')!
  const shownAt = Date.now()

  form.addEventListener('submit', async (event) => {
    event.preventDefault()
    const status = form.querySelector('[role="status"]')!
    const button = form.querySelector('button')!
    const data = new FormData(form)
    data.set('_elapsed', String(Date.now() - shownAt))
    button.disabled = true
    try {
      const response = await fetch(form.action, {
        method: 'POST',
        headers: { Accept: 'application/json' },
        body: data,
      })
      const result = await response.json()
      status.textContent = result.ok ? 'Thanks! We got your message.' : result.message
      if (result.ok) form.reset()
    } catch {
      status.textContent = 'Something went wrong. Please try again.'
    } finally {
      button.disabled = false
    }
  })
</script>
```

`Accept: application/json` gets a JSON answer instead of the thank-you page. `_elapsed` is how long the form was open, and submissions sent in under two seconds are dropped as bots.

With view transitions through Astro's `ClientRouter`, the script only runs on the first page load. Move the setup into `document.addEventListener('astro:page-load', ...)` so it runs after every navigation.



## Islands

If your site already uses React, Vue or Svelte, you can render one of those components as an island with `client:load` instead. The [React](/form-backend/react), [Vue](/form-backend/vue) and [Svelte](/form-backend/svelte) versions all post to the same endpoint.

Adding an adapter later for other pages doesn't change the form. When the site is live, add its domain to the form's [allowed websites](/docs/protection/access-rules) so other sites can't post to your endpoint.

## Guides

- [Astro forms](https://nisuform.com/docs/frameworks/astro)
- [Thank-you page and redirects](https://nisuform.com/docs/forms/after-submit)

## Frequently asked questions

### Can I use Astro Actions on a static site?

No. Actions run on the server, so they need an adapter and on-demand rendering. A form that posts to a form backend endpoint works on a fully static build, because the browser sends it straight to the endpoint.

### Does the form need JavaScript?

No. A plain HTML form with the endpoint as its action posts on its own and shows a thank-you page. The optional script keeps visitors on the page and shows the result in place.

### Can I send visitors back to my own thank-you page?

Yes, on Pro. Set a redirect URL in the form's settings, or add a hidden _redirect field with the full URL of your Astro thank-you page.

### Does the script work with View Transitions?

Yes, if it runs on every navigation. With Astro's ClientRouter, put the setup inside a listener for the astro:page-load event so the form is wired up again after each page swap.

### Can I use a React, Vue or Svelte component instead?

Yes. Add the framework's integration and render the component as an island with client:load. The React, Vue and Svelte versions post to the same endpoint.
