# Submit forms with JavaScript (AJAX)

> Formspree Docs · Building Your Form · Updated March 23, 2026

#### Available on: All plans

You can use [`@formspree/ajax`](https://www.npmjs.com/package/@formspree/ajax)[](https://www.npmjs.com/package/@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:

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

And, initialize your form in JavaScript:

```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:

```html
<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:

```html
<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.

```html
<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.

```html
<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.

```html
<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.

```html
<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.

```html
<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:

```javascript
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.

```javascript
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.

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

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

#### Available methods

  
| Method | When it runs | Common uses |
| --- | --- | --- |
| `disable(context)` | When submission starts | Add loading state, disable buttons/inputs |
| `enable(context)` | After submission completes | Remove spinners, re-enable UI |
| `renderFieldErrors(context, error)` | On validation errors | Custom error UI, grouped errors, toast messages |
| `renderSuccess(context, message)` | On successful submission | Replace form, show toast, redirect |
| `renderFormError(context, message)` | On form-level errors | Error banners, alerts, centralized messaging |

Here's how to use them:

```javascript
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.

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

```javascript
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:

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

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

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

```html
<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:

```html
<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](/articles/working-with-react/the-formspree-react-library/) instead.
