Skip to content

Astro Forms

Astro is great for static sites. Here’s how to wire up a contact form with Formkove.

In any .astro file:

---
// No server-side code needed. Formkove handles everything client-side
---
<form id="contact-form">
<input type="text" name="name" placeholder="Full name" required />
<input type="email" name="email" placeholder="Email address" required />
<textarea name="message" placeholder="Your message" required></textarea>
<button type="submit">Send Message</button>
</form>
<div id="form-result"></div>
<script>
const form = document.getElementById("contact-form");
const result = document.getElementById("form-result");
form?.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(form);
const data = Object.fromEntries(formData);
result.textContent = "Sending...";
try {
const res = await fetch("https://app.formkove.com/api/forms/YOUR_FORM_ID/submissions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(data)
});
const json = await res.json();
if (res.ok) {
result.textContent = "Message sent!";
form.reset();
} else {
result.textContent = json.error || "Something went wrong.";
}
} catch (err) {
result.textContent = "Network error. Please try again.";
}
});
</script>
---
// Full example with custom validation styling
---
<style>
.form-group { margin-bottom: 1rem; }
input, textarea { width: 100%; padding: 0.5rem; border: 1px solid #ccc; }
.error { color: red; font-size: 0.875rem; }
</style>
<form id="contact-form" novalidate>
<div class="form-group">
<input
type="text"
name="name"
placeholder="Full name"
required
/>
</div>
<div class="form-group">
<input
type="email"
name="email"
placeholder="Email address"
required
/>
</div>
<div class="form-group">
<textarea
name="message"
placeholder="Your message"
required
></textarea>
</div>
<button type="submit">Send Message</button>
<p id="result"></p>
</form>
<script>
const form = document.getElementById("contact-form");
const result = document.getElementById("result");
form?.addEventListener("submit", async (e) => {
e.preventDefault();
if (!form.checkValidity()) {
form.classList.add("was-validated");
return;
}
const formData = new FormData(form);
const data = Object.fromEntries(formData);
result.textContent = "Sending...";
try {
const res = await fetch("https://app.formkove.com/api/forms/YOUR_FORM_ID/submissions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(data)
});
const json = await res.json();
if (res.ok) {
result.textContent = "Sent! We'll be in touch.";
form.reset();
} else {
result.textContent = json.error || "Something went wrong.";
}
} catch (err) {
result.textContent = "Network error. Please try again.";
}
});
</script>
  • The form ID goes in the fetch URL, not as a hidden field
  • Use Content-Type: application/json with fetch
  • novalidate on the form lets you handle validation in JS
  • The <script> tag runs client-side, with no SSR needed for the form submission
  • Works with Astro’s view transitions if you use astro:page-load instead of DOMContentLoaded