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 yarnAnd, 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
idattribute of your form matches the ID that you have passed in as theformIdin theinitFormcall 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-successelement. - A form-level error message is shown in the
data-fs-errorelement. - If there is no
data-fs-successelement and no customonSuccesshandler, the form is replaced with a “Thank you!” message. - If there is a
data-fs-successelement and no customonSuccesshandler, 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
| 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:
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.