Contact form component
The key comes from the accessKey prop, then from PUBLIC_FORMGONG_KEY. Astro scopes the <style> block to this component, so the CSS does not leak into the rest of the site.
The script finds every form with data-formgong on the page. Using the component twice on one page is fine.
---
// Set PUBLIC_FORMGONG_KEY in .env, or pass accessKey="fk_…" where you use the component.
interface Props { accessKey?: string }
const { accessKey = import.meta.env.PUBLIC_FORMGONG_KEY ?? "fk_your_access_key" } = Astro.props;
---
<form class="contact-form" action="https://formgong.com/submit" method="POST" data-formgong>
<input type="hidden" name="access_key" value={accessKey} />
<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>
<div class="hp" aria-hidden="true"><input type="text" name="botcheck" tabindex="-1" autocomplete="off" /></div>
<button type="submit">Send message</button>
<p data-status role="status"></p>
</form>
<script>
for (const form of document.querySelectorAll<HTMLFormElement>("form[data-formgong]")) {
form.addEventListener("submit", async (event) => {
event.preventDefault();
const button = form.querySelector("button")!;
const status = form.querySelector("[data-status]")!;
button.disabled = true;
status.textContent = "";
try {
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const result: { success: boolean; message?: string } = await response.json();
if (result.success === true) {
form.reset();
status.textContent = "Thanks! Your message has been sent.";
} else {
status.textContent = result.message || "Not sent. Please try again.";
}
} catch {
status.textContent = "Not sent: check your connection and try again.";
}
button.disabled = false;
});
}
</script>
<style>
.contact-form { position: relative; display: grid; gap: 1rem; max-width: 32rem; }
label { display: grid; gap: 0.35rem; font-weight: 600; }
input, textarea { font: inherit; padding: 0.6rem 0.75rem; border: 1px solid #9aa5b1; border-radius: 8px; }
button { justify-self: start; padding: 0.7rem 1.2rem; border: 0; border-radius: 8px; background: #2f5bea; color: #fff; font: inherit; font-weight: 600; }
button:disabled { opacity: 0.6; }
.hp { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
</style>
Do I need Astro Actions or an endpoint?
No. Those need an adapter and a server, and a static site has neither. The form posts from the visitor's browser straight to Formgong. If your site already runs on a server, you can still use this component as it is.
How we tested this code
We pasted each snippet unchanged into a fresh project (Astro 7.4), filled in a real access key, and submitted it in Chromium three times against a local copy of Formgong. With the right key the form showed its success text and exactly one submission was stored, not marked as spam. With a key Formgong does not know, the form showed Formgong's own error, "That access key does not exist.", and nothing was stored. With the request cut off, the form said it was not sent. None of the snippets says "sent" before Formgong answers success: true.
astro check reported 0 errors and 0 warnings for both components.
Questions
Is it safe to put the access key in front-end code?
Yes. The key only lets someone send a submission to your form, the same thing your page already does. It cannot read submissions or change settings. If you want it out of the page source anyway, add your domain under allowed domains in the form settings, or post through your own server.
Does the form work without JavaScript?
Yes. It is a normal form with an action and method POST, so without the script it posts to Formgong and the visitor sees Formgong's thank-you page.
Can I use it with view transitions?
The script runs once per full page load. With the ClientRouter, wrap the loop in document.addEventListener("astro:page-load", …) so it runs after each navigation.
What is the hidden botcheck field?
A honeypot. It sits off screen, so people never fill it, while simple bots fill every field they find. Formgong marks a submission with it filled in as spam, so it does not reach your email. Keep the name botcheck.
Sources
Checked on 16.10.2026 against these public pages: Astro: Scripts and event handling, Astro: Environment variables.