Introducing @formspree/ajax: Declarative Form Handling in HTML

A lightweight JavaScript library for handling form submissions without boilerplate

in

For years, submitting forms with Formspree using JavaScript meant writing your own glue code.

You’d listen for submit events, serialize form data, send a request with fetch or Axios, handle validation errors, update the DOM, and wire up loading states. It worked, but it was repetitive. And every project ended up solving the same problem slightly differently.

Today, we’re introducing @formspree/ajax — a small, vanilla JavaScript library that removes that entire layer of boilerplate.

What is @formspree/ajax?

@formspree/ajax is a lightweight library for handling Formspree form submissions declaratively.

It works with standard HTML forms and gives you:

  • Automatic form submission
  • Built-in loading states
  • Field-level and form-level validation handling
  • Success messages
  • Accessible error states (aria-invalid)

No frameworks. No custom request handling. No repetitive boilerplates.

How it works

If you’ve ever submitted a Formspree form with JavaScript, you’ve probably written something like this:

  <form id="my-form" 
        action="https://formspree.io/f/YOUR_FORM_ID" 
        method="POST">
    <label>Email:</label>
    <input type="email" name="email" />
    <label>Message:</label>
    <input type="text" name="message" />
    <button id="my-form-button">Submit</button>
    <p id="my-form-status"></p>
  </form>

  <script>
    var form = document.getElementById("my-form");
    async function handleSubmit(event) {
      event.preventDefault();
      var status = document.getElementById("my-form-status");
      var data = new FormData(event.target);
      fetch(event.target.action, {
        method: form.method,
        body: data,
        headers: {
          'Accept': 'application/json'
        }
      }).then(response => {
        if (response.ok) {
          status.innerHTML = "Thanks for your submission!";
          form.reset()
        } else {
          response.json().then(data => {
            if (Object.hasOwn(data, 'errors')) {
              status.innerHTML = data["errors"]
                                  .map(error => error["message"])
                                  .join(", ")
            } else {
              status.innerHTML = "Oops! There was a problem submitting" 
              + "your form"
            }
          })
        }
      }).catch(error => {
        status.innerHTML = "Oops! There was a problem submitting your form"
      });
    }
    form.addEventListener("submit", handleSubmit)
  </script>

It works, but every project ends up repeating that same logic: handling loading state, parsing errors, updating the DOM.

Now, the same thing with @formspree/ajax

  import { initForm } from '@formspree/ajax';

  initForm({
    formElement: '#contact-form',
    formId: 'YOUR_FORM_ID',
  });
  <div data-fs-success></div>
  <div data-fs-error></div>

  <form id="contact-form">
    <input type="email" name="email" data-fs-field />
    <span data-fs-error="email"></span>

    <button type="submit" data-fs-submit-btn>Send</button>
  </form>

That’s it.

No manual fetch. No error parsing. No UI wiring. It handles:

  • Form submission
  • Loading state (button disable/enable)
  • Field and form-level validation errors
  • Success messages
  • Accessible error states (aria-invalid)

Try it out

Install:

  npm install @formspree/ajax

Or use it directly with a script tag.

👉 Read the full docs here.


Got Feedback?