Skip to content

Forminit + Astro Integration Guide

Forminit+Astro

Astro is a modern web framework that ships zero JavaScript by default and supports partial hydration for interactive components. Forminit integrates seamlessly with Astro’s “Islands Architecture” — keeping your forms lightweight while adding powerful backend functionality.

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 3.0+ (Astro 5.0+ recommended)
  • Node.js 18+
  • A Forminit account and Form ID

  1. Sign up or log in at forminit.com
  2. Create a new form
  3. Go to Form Settings → Set authentication mode to Public
  4. Copy your Form ID (e.g., YOUR-FORM-ID)

Astro components are static by default. For form handling, you have two options:

Option A: Client-Side Script (No Framework)

Section titled “Option A: Client-Side Script (No Framework)”
---
// src/components/ContactForm.astro
---

<form id="contact-form" class="contact-form">
  <div class="field">
    <label for="firstName">First Name</label>
    <input type="text" id="firstName" name="fi-sender-firstName" required />
  </div>

  <div class="field">
    <label for="lastName">Last Name</label>
    <input type="text" id="lastName" name="fi-sender-lastName" required />
  </div>

  <div class="field">
    <label for="email">Email</label>
    <input type="email" id="email" name="fi-sender-email" required />
  </div>

  <div class="field">
    <label for="message">Message</label>
    <textarea id="message" name="fi-text-message" rows="5" required></textarea>
  </div>

  <button type="submit">Send Message</button>
  <p id="form-status" class="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 Forminit SDK
  const script = document.createElement('script');
  script.src = 'https://forminit.com/sdk/v1/forminit.js';
  script.onload = () => {
    const forminit = new Forminit();

    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      
      status.textContent = 'Sending...';
      status.className = 'status loading';
      button.disabled = true;

      const formData = new FormData(form);
      const { data, error } = await forminit.submit(FORM_ID, formData);

      button.disabled = false;

      if (error) {
        status.textContent = error.message;
        status.className = 'status error';
        return;
      }

      status.textContent = 'Message sent successfully!';
      status.className = 'status success';
      form.reset();
    });
  };
  document.body.appendChild(script);
</script>

<style>
  .contact-form {
    max-width: 28rem;
  }
  .field {
    margin-bottom: 1rem;
  }
  label {
    display: block;
    margin-bottom: 0.25rem;
    font-weight: 500;
  }
  input, textarea {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #d1d5db;
    border-radius: 0.375rem;
  }
  button {
    background: #2563eb;
    color: white;
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 0.375rem;
    cursor: pointer;
  }
  button:hover { background: #1d4ed8; }
  button:disabled { background: #9ca3af; cursor: not-allowed; }
  .status { margin-top: 0.5rem; }
  .status.loading { color: #6b7280; }
  .status.success { color: #059669; }
  .status.error { color: #dc2626; }
</style>

Option B: React Island (Interactive Component)

Section titled “Option B: React Island (Interactive Component)”

If you prefer React, first install the Forminit package:

npm install forminit

Then create an interactive island:

// src/components/ContactFormReact.tsx

import { useState, useRef } from 'react';
import { Forminit } from 'forminit';

const FORM_ID = 'YOUR-FORM-ID'; // ← Replace with your Form ID

export default function ContactFormReact() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const formRef = useRef<HTMLFormElement>(null);
  const forminit = new Forminit();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!formRef.current) return;

    setStatus('loading');
    setErrorMessage(null);

    const formData = new FormData(formRef.current);
    const { data, error } = await forminit.submit(FORM_ID, formData);

    if (error) {
      setStatus('error');
      setErrorMessage(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" rows={5} required />
      
      {status === 'error' && <p className="error">{errorMessage}</p>}
      {status === 'success' && <p className="success">Message sent!</p>}
      
      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}

Use with client:load directive:

---
// src/pages/contact.astro
import ContactFormReact from '../components/ContactFormReact';
---

<ContactFormReact client:load />

Use with client:load directive:

---
// src/pages/contact.astro
import ContactFormReact from '../components/ContactFormReact';
---

<ContactFormReact client:load />

---
// src/pages/contact.astro
import Layout from '../layouts/Layout.astro';
import ContactForm from '../components/ContactForm.astro';
---

<Layout title="Contact Us">
  <main>
    <h1>Get in Touch</h1>
    <p>Have a project in mind? Let's talk about it.</p>
    <ContactForm />
  </main>
</Layout>

For a complete list of available form blocks (text, email, phone, file, rating, select, etc.) and field naming patterns, see the Form Blocks documentation.


Perfect for freelancers and agencies:

---
// src/components/QuoteRequestForm.astro
---

<form id="quote-form">
  <div class="grid">
    <div class="field">
      <label for="name">Your Name</label>
      <input type="text" id="name" name="fi-sender-fullName" required />
    </div>
    
    <div class="field">
      <label for="email">Email Address</label>
      <input type="email" id="email" name="fi-sender-email" required />
    </div>
  </div>
  
  <div class="field">
    <label for="company">Company (optional)</label>
    <input type="text" id="company" name="fi-sender-company" />
  </div>
  
  <div class="field">
    <label for="website">Current Website (if any)</label>
    <input type="url" id="website" name="fi-url-current_website" placeholder="https://" />
  </div>
  
  <div class="field">
    <label>Services Needed</label>
    <div class="checkbox-group">
      <label><input type="checkbox" name="fi-checkbox-services" value="web-design" /> Web Design</label>
      <label><input type="checkbox" name="fi-checkbox-services" value="development" /> Development</label>
      <label><input type="checkbox" name="fi-checkbox-services" value="seo" /> SEO</label>
      <label><input type="checkbox" name="fi-checkbox-services" value="branding" /> Branding</label>
      <label><input type="checkbox" name="fi-checkbox-services" value="maintenance" /> Maintenance</label>
    </div>
  </div>
  
  <div class="field">
    <label for="budget">Budget Range</label>
    <select id="budget" name="fi-select-budget" required>
      <option value="">Select your budget</option>
      <option value="under-5k">Under $5,000</option>
      <option value="5k-10k">$5,000 - $10,000</option>
      <option value="10k-25k">$10,000 - $25,000</option>
      <option value="25k-50k">$25,000 - $50,000</option>
      <option value="50k-plus">$50,000+</option>
    </select>
  </div>
  
  <div class="field">
    <label for="timeline">Ideal Timeline</label>
    <select id="timeline" name="fi-select-timeline">
      <option value="">When do you need this?</option>
      <option value="asap">ASAP</option>
      <option value="1-month">Within 1 month</option>
      <option value="1-3-months">1-3 months</option>
      <option value="3-6-months">3-6 months</option>
      <option value="flexible">Flexible</option>
    </select>
  </div>
  
  <div class="field">
    <label for="details">Project Details</label>
    <textarea id="details" name="fi-text-project_details" rows="6" placeholder="Tell us about your project, goals, and any specific requirements..." required></textarea>
  </div>
  
  <button type="submit">Request Quote</button>
  <p id="quote-status" aria-live="polite"></p>
</form>

<script>
  const FORM_ID = 'YOUR-FORM-ID'; // ← Replace with your Form ID
  // Same script pattern as above
</script>

For software products and open-source projects:

---
// src/components/BugReportForm.astro
---

<form id="bug-form">
  <div class="field">
    <label for="email">Your Email</label>
    <input type="email" id="email" name="fi-sender-email" required />
  </div>
  
  <div class="field">
    <label for="title">Bug Title</label>
    <input type="text" id="title" name="fi-text-bug_title" placeholder="Brief description of the issue" required />
  </div>
  
  <div class="field">
    <label for="severity">Severity</label>
    <select id="severity" name="fi-select-severity" required>
      <option value="">Select severity</option>
      <option value="critical">🔴 Critical - App crashes or data loss</option>
      <option value="high">🟠 High - Major feature broken</option>
      <option value="medium">🟡 Medium - Feature works but has issues</option>
      <option value="low">🟢 Low - Minor issue or cosmetic</option>
    </select>
  </div>
  
  <div class="field">
    <label for="version">App Version</label>
    <input type="text" id="version" name="fi-text-app_version" placeholder="e.g., 2.1.0" />
  </div>
  
  <div class="field">
    <label for="browser">Browser / Environment</label>
    <input type="text" id="browser" name="fi-text-browser" placeholder="e.g., Chrome 120, macOS Sonoma" />
  </div>
  
  <div class="field">
    <label for="steps">Steps to Reproduce</label>
    <textarea id="steps" name="fi-text-steps_to_reproduce" rows="4" placeholder="1. Go to...&#10;2. Click on...&#10;3. See error" required></textarea>
  </div>
  
  <div class="field">
    <label for="expected">Expected Behavior</label>
    <textarea id="expected" name="fi-text-expected_behavior" rows="2" placeholder="What should happen?" required></textarea>
  </div>
  
  <div class="field">
    <label for="actual">Actual Behavior</label>
    <textarea id="actual" name="fi-text-actual_behavior" rows="2" placeholder="What actually happens?" required></textarea>
  </div>
  
  <div class="field">
    <label for="screenshot">Screenshot (optional)</label>
    <input type="file" id="screenshot" name="fi-file-screenshot" accept="image/*" />
  </div>
  
  <div class="field">
    <label for="logs">Console Logs / Error Messages</label>
    <textarea id="logs" name="fi-text-console_logs" rows="3" placeholder="Paste any error messages here..."></textarea>
  </div>
  
  <button type="submit">Submit Bug Report</button>
  <p id="bug-status" aria-live="polite"></p>
</form>

For product launches:

---
// src/components/WaitlistForm.astro
---

<form id="waitlist-form" class="waitlist">
  <div class="field">
    <label for="email">Email Address</label>
    <input type="email" id="email" name="fi-sender-email" placeholder="you@company.com" required />
  </div>
  
  <div class="field">
    <label for="name">Name (optional)</label>
    <input type="text" id="name" name="fi-sender-fullName" placeholder="Your name" />
  </div>
  
  <div class="field">
    <label>What best describes you?</label>
    <select name="fi-select-persona" required>
      <option value="">Select one</option>
      <option value="indie-developer">Indie Developer</option>
      <option value="startup">Startup (1-10 employees)</option>
      <option value="small-business">Small Business (11-50)</option>
      <option value="mid-market">Mid-Market (51-500)</option>
      <option value="enterprise">Enterprise (500+)</option>
      <option value="agency">Agency / Freelancer</option>
      <option value="other">Other</option>
    </select>
  </div>
  
  <div class="field">
    <label>How did you hear about us?</label>
    <select name="fi-select-referral_source">
      <option value="">Select one</option>
      <option value="twitter">Twitter / X</option>
      <option value="linkedin">LinkedIn</option>
      <option value="hacker-news">Hacker News</option>
      <option value="product-hunt">Product Hunt</option>
      <option value="google">Google Search</option>
      <option value="friend">Friend / Colleague</option>
      <option value="blog">Blog Post</option>
      <option value="other">Other</option>
    </select>
  </div>
  
  <div class="field">
    <label for="use-case">What would you use this for? (optional)</label>
    <textarea id="use-case" name="fi-text-use_case" rows="2" placeholder="Help us understand your needs..."></textarea>
  </div>
  
  <button type="submit">Join Waitlist →</button>
  <p id="waitlist-status" aria-live="polite"></p>
  
  <p class="note">We'll notify you when early access is available. No spam, ever.</p>
</form>

<script>
  const FORM_ID = 'YOUR-FORM-ID'; // ← Replace with your Form ID
  const form = document.getElementById('waitlist-form');
  const status = document.getElementById('waitlist-status');
  const button = form.querySelector('button[type="submit"]');

  const script = document.createElement('script');
  script.src = 'https://forminit.com/sdk/v1/forminit.js';
  script.onload = () => {
    const forminit = new Forminit();

    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      
      status.textContent = 'Joining...';
      button.disabled = true;
      button.textContent = 'Joining...';

      const formData = new FormData(form);
      const { data, error } = await forminit.submit(FORM_ID, formData);

      if (error) {
        status.textContent = error.message;
        status.className = 'status error';
        button.disabled = false;
        button.textContent = 'Join Waitlist →';
        return;
      }

      // Replace form with success message
      form.innerHTML = `
        <div class="success-state">
          <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
            <polyline points="22 4 12 14.01 9 11.01"/>
          </svg>
          <h3>You're on the list!</h3>
          <p>We'll be in touch when early access is ready.</p>
        </div>
      `;
    });
  };
  document.body.appendChild(script);
</script>

The SDK returns { 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"
  }
}

const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);

if (error) {
  status.textContent = error.message;
  return;
}

// Redirect to thank-you page
window.location.href = '/thank-you';

Create a thank-you page:

---
// src/pages/thank-you.astro
import Layout from '../layouts/Layout.astro';
---

<Layout title="Thank You">
  <main class="thank-you">
    <h1>Thank You!</h1>
    <p>Your message has been received. We'll get back to you shortly.</p>
    <a href="/">← Back to Home</a>
  </main>
</Layout>

my-astro-site/
├── src/
│   ├── components/
│   │   ├── ContactForm.astro
│   │   ├── QuoteRequestForm.astro
│   │   ├── WaitlistForm.astro
│   │   └── ContactFormReact.tsx    # Optional React island
│   ├── layouts/
│   │   └── Layout.astro
│   └── pages/
│       ├── index.astro
│       ├── contact.astro
│       └── thank-you.astro
├── public/
├── astro.config.mjs
└── package.json

# Development
npm run dev

# Build
npm run build

# Preview production build
npm run preview

Deploy the dist/ folder to Netlify, Vercel, Cloudflare Pages, or any static host.


  1. View your submissions in the Forminit dashboard
  2. Set up email notifications for new submissions
  3. Explore webhook integrations for advanced workflows