⌘I

Submit forms with JavaScript (AJAX)

Updated March 23, 2026 ·
howtoform-setup
Also available in:

Available on: All plans

You can use @formspree/ajax to submit your Formspree forms with plain JavaScript, without writing your own fetch, Axios, or jQuery submission logic.

The library works with standard HTML forms and adds declarative form handling through data attributes or a small JavaScript API. It handles form submission, loading state, validation errors, and success messages automatically.

How it works

@formspree/ajax gives you a declarative way to wire up a form. Instead of manually listening for submit events and writing your own request handling, you mark up your form with data attributes and initialize it with your Formspree form ID.

By default, the library handles:

  • form submission
  • loading state on the submit button
  • field-level validation errors
  • form-level errors
  • success messages
  • invalid field state with aria-invalid="true"

Here’s how to implement it in your project:

Step 1: Set up @formspree-ajax in your project

You can use @formspree/ajax in two ways, depending on your setup:

  • With a bundler (ESM via npm/yarn)
  • Without a bundler (script tag/CDN)

Option 1: Using a bundler (ESM)

Install the library:

npm install @formspree/ajax   # via npm, or
yarn add @formspree/ajax      # via yarn

And, initialize your form in JavaScript:

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

initForm({
  formElement: '#contact-form',
  formId: 'YOUR_FORM_ID',
});

Make sure that the ID of your HTML form element matches the ID you pass in to the formElement property. And, replace YOUR_FORM_ID with the form ID that you receive in the Formspree dashboard.

Option 2: Using a script tag (no build step)

If you are not using a bundler, add this snippet before the closing </body> tag:

<script>
  window.formspree =
    window.formspree ||
    function () {
      (formspree.q = formspree.q || []).push(arguments);
    };
  formspree(
    'initForm', 
    { formElement: '#contact-form', formId: 'YOUR_FORM_ID' }
  );
</script>
<script src="https://unpkg.com/@formspree/ajax@1" defer></script>

Similar to the first option, ensure that the ID of your HTML form element matches the ID you pass in to the formElement property. And, replace YOUR_FORM_ID with the form ID that you receive in the Formspree dashboard.

Step 2: Build your form

Once you have set up @formspree/ajax in your project, this is how you can create a form with it:

<div data-fs-success></div>
<div data-fs-error></div>

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

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

Once again, please ensure that the value in the id attribute of your form matches the ID that you have passed in as the formId in the initForm call in the earlier step.

The library will automatically submit the form to Formspree, disable the submit button while the request is in flight, show validation errors, and display a success message.

Data attributes

The @formspree-ajax library ships with a few attributes that let you control how the form behaves without requiring extra JavaScript.

data-fs-error="fieldName"

Use this on a <span> or <div> to display field-level validation errors.

<span data-fs-error="email"></span>

If the element is empty, Formspree injects the API error message. If the element already contains content, that content is shown or hidden instead.

data-fs-error

Use this without a value to display form-level errors.

<div data-fs-error></div>

This is useful for general submission errors that do not belong to a specific field.

data-fs-success

Use this to show a success message after submission.

<div data-fs-success></div>

If the element is empty, the default message is shown. If it already has content, that content is used instead.

data-fs-field

Use this on form fields that should receive aria-invalid="true" when validation fails.

<input type="email" name="email" data-fs-field />

The library reads the field name from the element’s name attribute.

data-fs-submit-btn

Use this on the submit button, so it is disabled during submission and re-enabled afterward.

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

Default behavior

If you do not override anything, the library provides sensible defaults:

  • The submit button is disabled while the form is submitting.
  • Field errors are displayed inside matching data-fs-error="fieldName" elements.
  • Invalid fields receive aria-invalid="true".
  • A success message is shown in the data-fs-success element.
  • A form-level error message is shown in the data-fs-error element.
  • If there is no data-fs-success element and no custom onSuccess handler, the form is replaced with a “Thank you!” message.
  • If there is a data-fs-success element and no custom onSuccess handler, the form is reset after a successful submission.
  • Basic default styles are injected automatically.

Advanced Configuration

The library also offers ways to further customize your form’s behavior and looks.

Add extra data to every submission

You can use the data option to append additional values to every form submission. The values can be:

  • static strings
  • synchronous functions
  • asynchronous functions

Any undefined values are skipped. This is how to use it:

initForm({
  formElement: '#my-form',
  formId: 'YOUR_FORM_ID',

  // This data will be added to every submission
  data: {
    source: 'landing-page',
    referrer: () => document.referrer || undefined,
    sessionId: async () => await fetchSessionId(),
  },
});

It is useful for attaching metadata like campaign names, referral sources, or session IDs without adding hidden inputs to your HTML.

Lifecycle callbacks

You can hook into different stages of the submission lifecycle.

initForm({
  formElement: '#contact-form',
  formId: 'YOUR_FORM_ID',

  // Called when the form is initialized
  onInit: (context) => {
    console.log('Form initialized');
  },

  // Called before submission is sent.
  onSubmit: (context) => {
    console.log('Submitting form');
  },

  // Called on successful submission.
  onSuccess: (context, result) => {
    console.log('Submission succeeded', result);
  },

  // Called if validation errors are received from Formspree
  onError: (context, error) => {
    console.log('Validation error', error);
  },

  // Called if unexpected errors are received (e.g., network failure).
  onFailure: (context, error) => {
    console.log('Unexpected error', error);
  },
});

The context object contains the current form (HTMLFormElement), endpoint details, client, and the config object you passed to initForm

These callbacks can be used for:

  • Tracking events (analytics, conversions, drop-offs)
  • Showing custom UI (spinners, toasts, banners)
  • Redirecting after successful submission
  • Logging or debugging requests and failures
  • Custom handling of validation or form-level errors
  • Running side effects before or after submission (e.g., saving state, triggering workflows)

Custom rendering

You can override the default UI behavior by providing custom rendering methods in initForm(). This is useful if you want full control over how loading states, errors, and success messages appear.

initForm({
  formElement: '#contact-form',
  formId: 'YOUR_FORM_ID',

  enable: (context) => {},
  disable: (context) => {},
  renderFieldErrors: (context, error) => {},
  renderSuccess: (context, message) => {},
  renderFormError: (context, message) => {},
});

Available methods

MethodWhen it runsCommon uses
disable(context)When submission startsAdd loading state, disable buttons/inputs
enable(context)After submission completesRemove spinners, re-enable UI
renderFieldErrors(context, error)On validation errorsCustom error UI, grouped errors, toast messages
renderSuccess(context, message)On successful submissionReplace form, show toast, redirect
renderFormError(context, message)On form-level errorsError banners, alerts, centralized messaging

Here’s how to use them:

initForm({
  formElement: '#contact-form',
  formId: 'YOUR_FORM_ID',

  disable: ({ form }) => form.classList.add('loading'),
  enable: ({ form }) => form.classList.remove('loading'),

  renderSuccess: ({ form }, message) => {
    form.innerHTML = `<p>${message}</p>`;
  },
});

Styling

The library includes default styles for errors, invalid fields, and success messages. You can override them with your own CSS.

[data-fs-error] {
  color: #dc3455;
}

[data-fs-field][aria-invalid='true'] {
  border-color: #dc3455;
}

[data-fs-success][data-fs-active] {
  background: #de4dda;
}

[data-fs-error=''][data-fs-active] {
  background: #f87dda;
  color: #72c124;
}

Cleaning up

initForm() returns a handle with a destroy() method. You can use this if you need to clean up the form behavior later.

const handle = initForm({
  formElement: '#contact-form',
  formId: 'YOUR_FORM_ID',
});

// Clean up when needed
handle.destroy();

Complete ESM example

Here is a complete example using a plain HTML form and @formspree/ajax with a bundler. First, initialize the form with your Formspree form ID:

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

initForm({
  formElement: '#contact-form',
  formId: 'YOUR_FORM_ID',
});

Then, create the HTML form with the data attributes described above:

<div data-fs-success></div>
<div data-fs-error></div>

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

  <label for="message">Message</label>
  <textarea id="message" name="message" data-fs-field></textarea>
  <span data-fs-error="message"></span>

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

Script tag example

If you want to use the library without a build step, you can initialize it directly in the browser:

<div data-fs-success></div>
<div data-fs-error></div>

<form id="my-form">
  <label for="email">Email</label>
  <input type="email" id="email" name="email" data-fs-field />
  <span data-fs-error="email"></span>

  <label for="message">Message</label>
  <textarea id="message" name="message" data-fs-field></textarea>
  <span data-fs-error="message"></span>

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

<script>
  window.formspree =
    window.formspree ||
    function () {
      (formspree.q = formspree.q || []).push(arguments);
    };

  formspree('initForm', {
    formElement: '#my-form',
    formId: 'YOUR_FORM_ID',
  });
</script>
<script src="https://unpkg.com/@formspree/ajax@1" defer></script>

Migrating from manual AJAX code

If you have previously submitted forms with custom fetch, Axios, or jQuery code, @formspree/ajax gives you a simpler alternative. Instead of manually:

  • listening for submit events
  • serializing form data
  • sending requests
  • parsing validation errors
  • updating the DOM for success and failure states

You can initialize the library once and let it handle those pieces for you.

Framework note

This library is designed for vanilla JavaScript and standard HTML forms. If you are using React, check out our React library instead.

Still have questions? or .