Skip to content

Add forms to Astro

Forminit+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.

EXAMPLE PROMPT

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

Pick the approach that matches how your page is rendered.

ApproachBest forRenderingForminit mode
1. Client-side scriptStatic marketing sites, simple contact/waitlist formsStatic (prerender)Public
2. Framework islandYou already use React/Vue/Svelte/Solid componentsStatic + client:*Public
3. Astro ActionsHidden API key, server-side validation, works without JSOn-demand (adapter)Protected

If you’re not sure, start with Approach 1 — it works on any static host with no build configuration.


  1. Sign up or log in at forminit.com
  2. Create a new form
  3. Open Form Settings → Authentication mode:
    • Set Public for Approaches 1 & 2 (client-side)
    • Set Protected for Approach 3 (server-side, requires an API key)
  4. Copy your Form ID (e.g., YOUR_FORM_ID)

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>

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 />

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.

npm install forminit
npx astro add node

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
// 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.

---
// 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.


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.


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"
  }
}

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');
}

Forminit forms support multiple spam-protection layers — add them to any of the approaches above:


  • 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.env values are inlined at build time, so never read FORMINIT_API_KEY through import.meta.env in code that could reach the browser. Use astro:env/server (as in Approach 3) or process.env inside server-only action handlers — the key then only exists on the server.
  • ClientRouter’s handleForms prop 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.

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).

  1. View your submissions in the Forminit dashboard
  2. Set up email notifications for new submissions
  3. Explore webhook integrations for advanced workflows
  4. Read the Form Blocks reference to capture exactly the data you need