Form Integration

An Alpine.js contact form in one x-data component

If your page already loads Alpine from a script tag, you're most of the way there. x-data holds the fields and a loading flag, fetch sends them to Formbold, and each entry lands in your inbox. The Free plan includes 250 submissions a month.

Contact Form for Alpine.js

The x-data component

One x-data component, x-model on each field and a submit method that calls fetch. Paste it into any page that already loads Alpine 3, then replace FORM_ID.

alpinejs.html
<form
  x-data="contactForm()"
  @submit.prevent="submit"
  class="flex flex-col gap-4 max-w-[452px] mx-auto"
>
  <!-- Email Address -->
  <div class="flex flex-col gap-2">
    <label for="email" class="text-sm font-medium text-gray-700">
      Email Address<span class="ml-0.5 text-red-500">*</span>
    </label>
    <input
      type="email"
      name="email"
      id="email"
      x-model="formData.email"
      placeholder="Enter your email"
      required
      class="max-h-11 w-full rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-base text-gray-900 transition-all placeholder:text-[#9ca3af] focus:border-[#5750f1] focus:ring-4 focus:ring-[#5750f1]/10 focus:outline-none"
    />
  </div>

  <!-- Subject -->
  <div class="flex flex-col gap-2">
    <label for="subject" class="text-sm font-medium text-gray-700">
      Subject<span class="ml-0.5 text-red-500">*</span>
    </label>
    <input
      type="text"
      name="subject"
      id="subject"
      x-model="formData.subject"
      placeholder="Enter your subject"
      required
      class="max-h-11 w-full rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-base text-gray-900 transition-all placeholder:text-[#9ca3af] focus:border-[#5750f1] focus:ring-4 focus:ring-[#5750f1]/10 focus:outline-none"
    />
  </div>

  <!-- Message -->
  <div class="flex flex-col gap-2">
    <label for="message" class="text-sm font-medium text-gray-700">
      Message<span class="ml-0.5 text-red-500">*</span>
    </label>
    <textarea
      name="message"
      id="message"
      x-model="formData.message"
      rows="5"
      placeholder="Type your message..."
      required
      class="w-full resize-y rounded-xl border border-gray-300 bg-white px-4 py-3 text-base text-gray-900 transition-all placeholder:text-[#9ca3af] focus:border-[#5750f1] focus:ring-4 focus:ring-[#5750f1]/10 focus:outline-none"
    ></textarea>
  </div>

  <!-- Submit Button -->
  <button
    type="submit"
    :disabled="loading"
    class="flex h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-xl bg-[#111827] px-5 py-3 text-base font-medium text-white shadow-xs transition-all hover:bg-black active:scale-[0.99] disabled:opacity-50"
  >
    <span x-text="loading ? 'Submitting...' : 'Submit Message'"></span>
  </button>

  <!-- Status Message -->
  <div
    x-show="status"
    x-text="status"
    class="rounded-xl p-3 text-center text-sm font-medium bg-gray-50 text-gray-800"
  ></div>
</form>

<script>
  function contactForm() {
    return {
      loading: false,
      status: "",
      formData: { email: "", subject: "", message: "" },
      async submit() {
        this.loading = true;
        try {
          const response = await fetch("https://formbold.com/s/FORM_ID", {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Accept: "application/json",
            },
            body: JSON.stringify(this.formData),
          });
          const result = await response.json();
          if (response.ok) {
            this.status = result.message || "Message sent successfully!";
            this.formData = { email: "", subject: "", message: "" };
          } else {
            this.status = result.message || "Something went wrong";
          }
        } catch (err) {
          this.status = "Failed to send message.";
        } finally {
          this.loading = false;
          setTimeout(() => (this.status = ""), 4000);
        }
      },
    };
  }
</script>
How it works

How do I wire Alpine.js to Formbold?

Three steps, and no build step if you load Alpine from a CDN.

1

Create the form

Click Create New Form in the dashboard. Call it something like Alpine contact so it's easy to spot later.

2

Drop in your endpoint

The fetch call inside contactForm() needs your https://formbold.com/s/FORM_ID address. You will find it on the Form Setup tab.

3

Send a test

Submit the form and open Submissions. The keys in formData become the field labels you see there.

Features

What does Formbold add to an Alpine form?

Alpine stays in charge of the UI. Storage, email, spam checks and integrations happen on Formbold's side.

Static hosting is fine

There's no endpoint of your own to deploy or patch, so Netlify, GitHub Pages or a plain storage bucket all work.

Spam settings for a fetch form

Turn on Basic Spam Filter. Alpine submits with fetch, so Custom reCAPTCHA is the captcha option that works here.

File uploads

From Starter. Switch the body to FormData for files, since x-model values sent as JSON can't carry them.

Send it on to Slack or Notion

On Starter and up, connect Slack, Notion, Google Sheets, Telegram or Discord from the Integrations tab, or use a webhook.

FAQs

What else comes up with Alpine.js?

A few answers that save a round of debugging.

Only if you send JSON from formData, as this snippet does. A field that isn't in formData never reaches Formbold. The other option is @submit.prevent="submit($event)" and new FormData(event.target), which reads every named input.

Still have a question?

The docs cover setup, framework guides, integrations and spam protection, step by step.