Add forms to Astro
![]()
Astro ships zero JavaScript by default and renders pages three different ways — fully static, hydrated islands, and on-demand server routes. Forminit fits all three: keep a contact form static and lightweight, drop in a React island when you need interactivity, or handle submissions server-side with Astro Actions so your API key never touches the browser.
Build with AI
Use our LLM-ready skills to integrate Forminit with your form for faster integration. Edit the example prompt with your needs and paste your Form ID. Your AI tool will handle the rest.
Build me a contact form with name, email, and message fields. Connect it with Forminit for form submissions.
formId: <PASTE-YOUR-FORM-ID-HERE>
Use this integration skill guide: https://forminit.com/skills/forminit-html/SKILL.md
What you’ll get:
- Accept form submissions without building a backend
- Email notifications for new submissions
- Spam protection (reCAPTCHA, hCaptcha, honeypot)
- File uploads support
- Submission dashboard to manage responses
Requirements:
- Astro 6 (Node.js 22.12+) recommended. Client-side approaches work back to Astro 3.0; Astro Actions need Astro 4.15+ and a server adapter.
- A Forminit account and Form ID
Choose your integration
Section titled “Choose your integration”Pick the approach that matches how your page is rendered.
| Approach | Best for | Rendering | Forminit mode |
|---|---|---|---|
| 1. Client-side script | Static marketing sites, simple contact/waitlist forms | Static (prerender) | Public |
| 2. Framework island | You already use React/Vue/Svelte/Solid components | Static + client:* | Public |
| 3. Astro Actions | Hidden API key, server-side validation, works without JS | On-demand (adapter) | Protected |
If you’re not sure, start with Approach 1 — it works on any static host with no build configuration.
Step 1: Create a Form on Forminit
Section titled “Step 1: Create a Form on Forminit”- Sign up or log in at forminit.com
- Create a new form
- Open Form Settings → Authentication mode:
- Set Public for Approaches 1 & 2 (client-side)
- Set Protected for Approach 3 (server-side, requires an API key)
- Copy your Form ID (e.g.,
YOUR_FORM_ID)
Approach 1: Client-Side Script
Section titled “Approach 1: Client-Side Script”The simplest path. Your .astro component stays static and loads the Forminit SDK from the CDN — no npm dependency, no build step, works on any static host. Uses Public mode.
---
// src/components/ContactForm.astro
---
<form id="contact-form" class="contact-form">
<input type="text" name="fi-sender-firstName" placeholder="First name" required />
<input type="text" name="fi-sender-lastName" placeholder="Last name" required />
<input type="email" name="fi-sender-email" placeholder="Email" required />
<textarea name="fi-text-message" placeholder="Message" rows="5" required></textarea>
<button type="submit">Send Message</button>
<p id="form-status" aria-live="polite"></p>
</form>
<script>
const FORM_ID = 'YOUR_FORM_ID'; // ← Replace with your Form ID
const form = document.getElementById('contact-form');
const status = document.getElementById('form-status');
const button = form.querySelector('button[type="submit"]');
// Load the Forminit SDK from the CDN
const sdk = document.createElement('script');
sdk.src = 'https://forminit.com/sdk/v1/forminit.js';
sdk.onload = () => {
const forminit = new Forminit();
form.addEventListener('submit', async (e) => {
e.preventDefault();
status.textContent = 'Sending...';
button.disabled = true;
const { data, error } = await forminit.submit(FORM_ID, new FormData(form));
button.disabled = false;
if (error) {
status.textContent = error.message;
return;
}
status.textContent = 'Message sent successfully!';
form.reset();
});
};
document.body.appendChild(sdk);
</script>
Every input name follows the fi-{blockType}-{name} pattern. This example uses a handful of blocks — see the Form Blocks reference for all of them (number, select, radio, checkbox, rating, date, file, country, and more).
Add it to any page:
---
// src/pages/contact.astro
import Layout from '../layouts/Layout.astro';
import ContactForm from '../components/ContactForm.astro';
---
<Layout title="Contact Us">
<h1>Get in Touch</h1>
<ContactForm />
</Layout>
Approach 2: Framework Island
Section titled “Approach 2: Framework Island”Already using a UI framework in Astro? Build the form as a hydrated island with the npm SDK. Also uses Public mode (the component runs in the browser).
First install the SDK:
npm install forminit
Create the island (React shown — the same idea applies to Vue, Svelte, and Solid):
// src/components/ContactForm.tsx
import { useState, useRef, FormEvent } from 'react';
import { Forminit } from 'forminit';
const forminit = new Forminit();
const FORM_ID = 'YOUR_FORM_ID';
export default function ContactForm() {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [error, setError] = useState<string | null>(null);
const formRef = useRef<HTMLFormElement>(null);
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus('loading');
const { error } = await forminit.submit(FORM_ID, new FormData(e.currentTarget));
if (error) {
setStatus('error');
setError(error.message);
return;
}
setStatus('success');
formRef.current?.reset();
}
return (
<form ref={formRef} onSubmit={handleSubmit}>
<input type="text" name="fi-sender-firstName" placeholder="First name" required />
<input type="text" name="fi-sender-lastName" placeholder="Last name" required />
<input type="email" name="fi-sender-email" placeholder="Email" required />
<textarea name="fi-text-message" placeholder="Message" required />
{status === 'error' && <p className="error">{error}</p>}
{status === 'success' && <p className="success">Message sent!</p>}
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Sending...' : 'Send'}
</button>
</form>
);
}
Hydrate it on the page with a client directive:
---
// src/pages/contact.astro
import ContactForm from '../components/ContactForm.tsx';
---
<ContactForm client:load />
Approach 3: Astro Actions (Server-Side)
Section titled “Approach 3: Astro Actions (Server-Side)”The modern, Astro-native pattern. Actions handle the submission on the server, so your Protected-mode API key stays out of the browser, you get type-safe validation with Zod, and the form works even with JavaScript disabled (progressive enhancement).
Requirements: a server adapter (Node, Netlify, Vercel, Cloudflare…) and an on-demand rendered page.
1. Install the SDK and an adapter
Section titled “1. Install the SDK and an adapter”npm install forminit
npx astro add node
2. Store the API key as a server secret
Section titled “2. Store the API key as a server secret”Create an API token in Account → API Tokens, then declare it with Astro’s type-safe astro:env so it’s only ever available on the server:
// astro.config.mjs
import { defineConfig, envField } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
adapter: node({ mode: 'standalone' }),
env: {
schema: {
FORMINIT_API_KEY: envField.string({ context: 'server', access: 'secret' }),
},
},
});
# .env
FORMINIT_API_KEY=fi_your_secret_api_key
3. Define the action
Section titled “3. Define the action”// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
import { Forminit } from 'forminit';
import { FORMINIT_API_KEY } from 'astro:env/server';
const forminit = new Forminit({ apiKey: FORMINIT_API_KEY });
const FORM_ID = 'YOUR_FORM_ID';
export const server = {
contact: defineAction({
accept: 'form',
input: z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.email(),
message: z.string().min(1),
}),
handler: async ({ firstName, lastName, email, message }) => {
const { data, redirectUrl, error } = await forminit.submit(FORM_ID, {
blocks: [
{ type: 'sender', properties: { firstName, lastName, email } },
{ type: 'text', name: 'message', value: message },
],
});
if (error) throw new Error(error.message);
return { hashId: data.hashId, redirectUrl };
},
}),
};
Because the page POSTs straight to the action, the field names are plain (email, message) — Zod validates them and your handler maps them to Forminit blocks server-side.
4. Wire up the form
Section titled “4. Wire up the form”---
// src/pages/contact.astro
export const prerender = false; // on-demand render so the action can run
import Layout from '../layouts/Layout.astro';
import { actions } from 'astro:actions';
// On success, send the user to the redirect URL from your Forminit settings
const result = Astro.getActionResult(actions.contact);
if (result && !result.error) {
return Astro.redirect(result.data.redirectUrl ?? '/thank-you');
}
---
<Layout title="Contact Us">
<form method="POST" action={actions.contact}>
<input type="text" name="firstName" placeholder="First name" required />
<input type="text" name="lastName" placeholder="Last name" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required></textarea>
<button type="submit">Send Message</button>
</form>
{result?.error && <p class="error">Something went wrong. Please try again.</p>}
</Layout>
That’s it — no client-side <script> required. The form submits with a normal POST and works without JavaScript; add a client:* island on top only if you want inline, no-reload feedback.
Form Field Reference
Section titled “Form Field Reference”For every block type (text, number, email, phone, url, select, radio, checkbox, rating, date, file, country, plus the built-in sender and tracking), naming patterns, and validation rules, see the Form Blocks documentation.
Handling the Response
Section titled “Handling the Response”Every approach returns the same shape — { data, redirectUrl, error }:
On success:
{
data: {
hashId: "7LMIBoYY74JOCp1k",
date: "2026-01-01 21:10:24",
blocks: {
sender: { firstName: "John", lastName: "Doe", email: "john@example.com" },
message: "Hello from Astro!"
}
},
redirectUrl: "https://forminit.com/thank-you"
}
On error:
{
error: {
error: "ERROR_CODE",
code: 400,
message: "Human-readable error message"
}
}
Redirect After Submission
Section titled “Redirect After Submission”Every successful submission returns a redirectUrl you configure in Form Settings → Redirections — so you can change the thank-you destination anytime without editing code or redeploying.
Client-side (Approaches 1 & 2):
const { redirectUrl, error } = await forminit.submit(FORM_ID, new FormData(form));
if (!error) window.location.href = redirectUrl;
Astro Actions (Approach 3): redirect from the page using the value the handler returned (shown in step 4):
const result = Astro.getActionResult(actions.contact);
if (result && !result.error) {
return Astro.redirect(result.data.redirectUrl ?? '/thank-you');
}
Spam Protection
Section titled “Spam Protection”Forminit forms support multiple spam-protection layers — add them to any of the approaches above:
Astro 6 Notes
Section titled “Astro 6 Notes”- Node.js 22.12+ is required. Astro 6 dropped Node 18 and 20.
- Keep your API key off the client. In Astro 6,
import.meta.envvalues are inlined at build time, so never readFORMINIT_API_KEYthroughimport.meta.envin code that could reach the browser. Useastro:env/server(as in Approach 3) orprocess.envinside server-only action handlers — the key then only exists on the server. ClientRouter’shandleFormsprop was removed. If you used view transitions to intercept form posts, Astro Actions are now the supported way to handle submissions.- Client-side Approaches 1 & 2 are unaffected by these changes and run on any Astro version from 3.0 onward.
Build and Deploy
Section titled “Build and Deploy”npm run dev # Development
npm run build # Production build
npm run preview # Preview the build
- Approaches 1 & 2 produce a fully static site — deploy
dist/to Netlify, Vercel, Cloudflare Pages, GitHub Pages, or any static host. - Approach 3 needs the server adapter you installed — deploy to a host that runs the on-demand output (Netlify, Vercel, Cloudflare, or a Node server).
Next Steps
Section titled “Next Steps”- View your submissions in the Forminit dashboard
- Set up email notifications for new submissions
- Explore webhook integrations for advanced workflows
- Read the Form Blocks reference to capture exactly the data you need
Was this page helpful?
Thanks for your feedback.