When building a static site with Astro, the question of the contact form inevitably arises. Without a server (backend) to process the email sending, what do you do?
The most popular solution is called Formspree (or Web3Forms, Netlify Forms, etc.). You put a specific URL in the action attribute of your HTML form, and their service takes care of sending you the email. Magic.
Except there is a huge UX problem.
By default, when the user clicks “Send”, they are violently redirected to a generic thank-you page hosted by Formspree, or they suffer a full page reload if using a redirect parameter. Goodbye, seamless navigation.
Today, we are going to fix that. We are going to intercept the form submission with JavaScript (AJAX) to send the data in the background, and display a beautiful success message without ever leaving the page.
1. The HTML Structure (The Astro Component)
Let’s start by creating our contact form in a component (e.g., ContactForm.astro).
The trick here is to prepare a container for the entire form, and another one (hidden by default) for the success message.
---
// src/components/ContactForm.astro
// Replace this URL with the one provided by Formspree
const FORMSPREE_URL = "[https://formspree.io/f/YOUR_ID_HERE](https://formspree.io/f/YOUR_ID_HERE)";
---
<div id="contact-container">
<form id="contact-form" action={FORMSPREE_URL} method="POST">
<label for="email">Your Email</label>
<input type="email" id="email" name="email" required />
<label for="message">Your Message</label>
<textarea id="message" name="message" required></textarea>
<button type="submit" id="submit-button">Send message</button>
<p id="error-message" style="display: none; color: red;">Oops, an error occurred.</p>
</form>
<div id="success-message" style="display: none;">
<h3>✨ Message sent successfully!</h3>
<p>Thank you for your message, I will get back to you very soon.</p>
</div>
</div>
2. The Magic of Interception (JavaScript)
In Astro, all you have to do is add a <script> tag at the end of our component. Astro will automatically isolate it, minify it, and execute it on the client side.
The goal of our script is simple:
- Listen for the form’s
submitevent. - Block the default behavior (the infamous redirect) with
e.preventDefault(). - Retrieve the data via
FormData. - Send it silently with the native
fetch()function.
<script>
const form = document.getElementById('contact-form') as HTMLFormElement;
const successMessage = document.getElementById('success-message');
const errorMessage = document.getElementById('error-message');
const submitButton = document.getElementById('submit-button') as HTMLButtonElement;
if (form) {
form.addEventListener('submit', async (e) => {
// 1. Block the wild redirect!
e.preventDefault();
// 2. Change the button state to make the user wait
submitButton.disabled = true;
submitButton.innerText = "Sending...";
errorMessage.style.display = 'none';
try {
// 3. Prepare the data
const formData = new FormData(form);
// 4. Send the AJAX POST request
const response = await fetch(form.action, {
method: form.method,
body: formData,
headers: {
'Accept': 'application/json' // Crucial for Formspree
}
});
if (response.ok) {
// 5. It's a success! Hide the form and display the message
form.style.display = 'none';
if (successMessage) successMessage.style.display = 'block';
} else {
throw new Error('Network error');
}
} catch (error) {
// In case of a glitch, reactivate the button and display the error
if (errorMessage) errorMessage.style.display = 'block';
submitButton.disabled = false;
submitButton.innerText = "Send message";
}
});
}
</script>
The Killer Detail: The Accept Header
If you look closely at the fetch() code, I added an Accept: 'application/json' header. Without it, Formspree will still try to send you HTML back (their thank-you page). By specifying that we want JSON, Formspree understands that we are making an AJAX request and responds cleanly in the background with a simple { "ok": true }.
Conclusion
In a few dozen lines of “Vanilla” JavaScript (without any heavy libraries), we have just transformed a frustrating user experience into a fluid and modern interaction.
That is the philosophy of a good Project Manager / Developer: using simple tools to move fast (Formspree), while mastering the technical side to guarantee a professional, user-centric result.
Don’t believe me? Go test it all live on the Contact page of my site!