AJAX Forms with Freeform
Freeform includes built-in AJAX support for single-page and multi-page forms. To enable it, turn on Use AJAX under Settings → Processing in the form builder. Freeform will handle submissions, validation feedback, page changes, and the configured success behavior without a full page reload.
The Freeform JavaScript plugin powers AJAX submissions and other client-side features, including date fields, conditional rules, and Stripe payments. You can customize its messages, styling, and behavior using the options and examples below.
If you need to handle requests yourself, see Manual Implementations. For React, Vue, Next.js, or Nuxt projects, start with the Headless documentation.
Having trouble with a form's appearance, behavior, or submissions? See Troubleshooting Form Issues.
Caveats
- Stripe Payments: Freeform automatically enables AJAX for forms using Stripe Payments so it can handle payment validation and processing.
- Custom Markup: AJAX submissions can replace your form's contents with HTML rendered from the formatting template selected in the form builder. If you render your own markup, see Not Using a Formatting Template to preserve it.
- Submission Editing: Editing existing submissions through multi-page AJAX forms on the front end is currently not supported reliably.
- Multiple Instances: When rendering the same form more than once on a page, give each instance a unique form ID,
fieldIdPrefix, and form instance name. See Multiple Instances of the Same Form.
CustomizationImproved in 5.17+
Customize AJAX success and error messages, styling, and rendering using the examples below. Success and error messages can also be configured directly in the form builder.
For additional options and events, see the Freeform JavaScript plugin documentation.
Success & Error StylingImproved in 5.17+
Freeform's built-in AJAX feedback now follows the styling configured by your formatting template, making success messages and validation errors more consistent between AJAX and regular submissions.
When using the default AJAX renderer:
- Success and error banners inherit the resolved
attributes.successandattributes.errorssettings, including CSS classes and supported HTML attributes. - Field error messages reuse the formatting template's error-message styles when the required markup is available in the AJAX response.
- Field error states reflect supported
fields[":errors"]class changes on matching containers, labels, and inputs. - Existing inputs and entered values are preserved while their error styling is updated.
Existing JavaScript Customizations
Explicit JavaScript banner-class overrides and custom error-list classes continue to take precedence over inherited classes in the default renderer. The errorClassField option adds your chosen input class alongside template-derived error styles.
Templates with their own rendering handlers, including the current Tailwind 4 and daisyUI templates, control their banner output directly. Their markup and styling should be customized through the template or its rendering handler.
Use Template Overrides when you want shared styling for AJAX and regular submissions. Keep JavaScript overrides when you need specific AJAX behavior.
No template migration is required. See Updating Custom Templates to 5.17 below for guidance on sites that rely on the previous default AJAX classes.
Updating Custom Templates to 5.17
Freeform 5.17 lets AJAX success banners, error banners, and field errors follow the styling configured by your formatting template.
There is no required template migration. Existing custom templates can continue to be used. Explicit JavaScript banner-class overrides and custom error-list classes remain supported by the default renderer.
The main compatibility concern is CSS or JavaScript that assumes the previous default classes without explicitly configuring them.
| Your setup | What changes in 5.17 | Risk | What should you do |
|---|---|---|---|
| Bundled sample template, without custom feedback code | AJAX feedback follows the template's styling. | Low | No template edits required. Check the appearance after updating. |
| Custom styles defined in your Twig template | AJAX feedback can now use those styles too. | Low | Nothing if you like the result of any minor appearance changes. Adjust styling only if desired. |
| Banner classes or a custom error-list class explicitly set in JavaScript | The default renderer continues using your chosen classes. | Low | Usually nothing. Check the result for those selectors after updating. |
| CSS or JavaScript targeting the old default classes, without setting them explicitly | Appearance may change some banner or error-list classes may change, so selectors might stop matching. | Review recommended | Update selectors or explicitly configure the classes only if something is affected. |
| Custom JavaScript that renders its own feedback | Your code continues controlling its output. Code reading Freeform's class options may receive multiple classes. | Depends on the handler | No automatic rewrite required. Check the handler and adjust class handling if necessary. |
As part of normal upgrade testing, try one successful AJAX submission and one with validation errors. Make changes only if you find an issue or want to adopt newer sample-template features. Comparing or re-cloning every custom template is not required.
Notes on JavaScript Overrides
These overrides apply to the default renderer. Templates with their own rendering handlers control their output directly.
successClassBanneranderrorClassBanneroverride inherited banner classes.- A custom
errorClassListoverrides the inherited error-list class. Currently, setting it to the default stringfreeform-errorsstill allows inheritance; it does not lock the old class. errorClassFieldadds your input class alongside template-derived error styles rather than replacing them.
Customize Success & Error Messages
You can customize success and error messages directly in the form builder for each form. Alternatively, use the JavaScript overrides below to set messages for all forms on a page or for a specific form.
- All Forms
- Specific Form
document.addEventListener('freeform-ready', (event) => {
event.options.successBannerMessage = 'My custom success message';
event.options.errorBannerMessage = 'My custom error message';
});
const form = document.getElementById('my-form');
form.addEventListener('freeform-ready', (event) => {
event.options.successBannerMessage = 'My custom success message';
event.options.errorBannerMessage = 'My custom error message';
});
Customize Success & Error Banner Styles
Freeform 5.17 uses your formatting template's styles for default AJAX feedback. If you need different AJAX classes, you can override them during the freeform-ready event:
- All Forms
- Specific Form
document.addEventListener('freeform-ready', function (event) {
event.options.successClassBanner = 'my-custom-success-banner';
event.options.errorClassBanner = 'my-custom-error-banner';
event.options.errorClassList = 'my-custom-errors-list';
event.options.errorClassField = 'this-field-has-errors';
});
const form = document.getElementById('my-form');
form.addEventListener('freeform-ready', function (event) {
event.options.successClassBanner = 'my-custom-success-banner';
event.options.errorClassBanner = 'my-custom-error-banner';
event.options.errorClassList = 'my-custom-errors-list';
event.options.errorClassField = 'this-field-has-errors';
});
Only set the options you need. Your stylesheet must provide the corresponding styles.
For the default renderer, banner-class overrides and custom error-list classes take precedence over inherited classes. The input error class is added alongside template-derived error-state styles.
If your formatting template supplies its own banner-rendering handler, customize that handler or its Twig markup instead. For fully custom output, see the rendering events in the JavaScript Plugin documentation.
Display Success Banner at Bottom
Display success banners at the bottom of all forms on a page or a specific form. These examples use the configured message and banner classes and mark banners for automatic removal before subsequent submissions.
- All Forms
- Specific Form
document.addEventListener('freeform-render-success', (event) => {
event.preventDefault();
const { options } = event.freeform;
const banner = document.createElement('div');
banner.className = options.successClassBanner || '';
banner.textContent = options.successBannerMessage || '';
banner.setAttribute('role', 'status');
banner.setAttribute('data-freeform-ajax-banner', 'success');
event.form.appendChild(banner);
});
{{ freeform.form('myForm', { id: 'my-form' }).render }}
<script>
const form = document.getElementById('my-form');
form.addEventListener('freeform-render-success', (event) => {
event.preventDefault();
const { options } = event.freeform;
const banner = document.createElement('div');
banner.className = options.successClassBanner || '';
banner.textContent = options.successBannerMessage || '';
banner.setAttribute('role', 'status');
banner.setAttribute('data-freeform-ajax-banner', 'success');
form.appendChild(banner);
});
</script>
For a specific form, replace myForm with its handle and ensure my-form is unique on the page.
If your formatting template already provides a custom success-rendering handler, adjust that handler to place its banner at the bottom rather than adding a second handler.
Override Form Builder Success Behavior
To redirect after a successful AJAX submission—even when Success Behavior is set to Reload Form with Success Message—set the form's returnUrl parameter and add one of the following event listeners.
These examples redirect only after the final submission, allowing users to complete all pages of a multi-page form.
- All Forms
- Specific Form
Listen for successful AJAX submissions from all forms on the page. Set a return URL for each form you want to redirect.
{{ freeform.form('myFormHandle', {
returnUrl: 'contact/thank-you'
}).render() }}
<script>
document.addEventListener('freeform-ajax-success', (event) => {
const { finished, returnUrl } = event.response;
if (finished && returnUrl) {
window.location.href = returnUrl;
}
});
</script>
Listen for successful AJAX submissions from a specific form.
{{ freeform.form('myFormHandle', {
id: 'my-form',
returnUrl: 'contact/thank-you'
}).render() }}
<script>
const form = document.getElementById('my-form');
form.addEventListener('freeform-ajax-success', (event) => {
const { finished, returnUrl } = event.response;
if (finished && returnUrl) {
window.location.href = returnUrl;
}
});
</script>
Replace myFormHandle with your form's handle and contact/thank-you with your destination URL. For a specific form, ensure its id is unique on the page.
Not Using a Formatting Template
If you render a form using custom markup instead of a formatting template, Freeform's AJAX HTML replacement may change its appearance or structure. After a successful AJAX submission, Freeform can replace the form's contents with HTML rendered from the formatting template selected in the form builder.
To preserve your custom markup, add the data-skip-html-reload attribute to the form. This prevents Freeform from replacing the form's contents while allowing normal AJAX validation, field resetting, and success messages to continue.
You can add this attribute in either of the following ways:
- Form Builder
- Template
- Open the form's settings tab (cog/gear icon).
- Under Form tag Attributes, add
data-skip-html-reloadas the Attribute and leave the Value empty. - Save the form.
Add the attribute to your Form query:
{{ freeform.form('myFormHandle', {
formAttributes: {
'data-skip-html-reload': true
}
}).render() }}
If you manually write the opening <form> tag, add the attribute directly:
<form data-skip-html-reload>
This attribute disables HTML replacement during AJAX submissions, including page changes in multi-page forms. If your custom form has multiple pages, you must handle those page changes yourself.
Advanced
Listen to events dispatched by the Freeform JS plugin to add custom behavior after an AJAX submission.
The following example listens to a specific form. To apply these listeners to all forms on the page, replace form.addEventListener with document.addEventListener.
const form = document.getElementById('my-form');
form.addEventListener('freeform-ajax-success', (event) => {
const { finished, returnUrl, duplicate } = event.response;
if (duplicate) {
// Add your own handling for duplicate submissions.
console.log('Duplicate submission detected.');
}
// Redirect only after the final successful submission.
if (finished && returnUrl) {
window.location.href = returnUrl;
}
});
form.addEventListener('freeform-ajax-error', (event) => {
console.log('Field errors:', event.errors || {});
console.log('Form errors:', event.formErrors || []);
console.log('Response:', event.response);
});
form.addEventListener('freeform-ajax-after-submit', (event) => {
console.log('AJAX response processed:', event.response);
});
freeform-ajax-success can also fire when a multi-page form advances to its next page. Check event.response.finished before running code intended for a completed submission.
freeform-ajax-after-submit runs after Freeform processes a response. Redirects, canceled processing, and request failures may bypass it, so use freeform-ajax-error to handle failures.
Manual Implementations
You can submit a rendered Freeform form using your own JavaScript request handler. If you only need to customize Freeform's built-in AJAX behavior, use the event listeners above.
For React, Vue, Next.js, or Nuxt projects, see the Headless documentation for the official packages and API options.
Before adding a manual submit handler, set Script Insert Location to None and ensure Freeform's built-in submit handler is not loaded separately.
You are responsible for the client-side functionality normally provided by Freeform, including validation messages, page changes, CAPTCHA, payment actions, and advanced field behavior.
data-skip-html-reload only prevents HTML replacement by the built-in plugin. It does not disable its submit handler or prevent duplicate requests.
The examples below submit a basic, single-page form on the same site. Keep Freeform's generated hidden inputs, including its payload and Craft CSRF token, inside the form.
If you use cached forms or asynchronous CSRF inputs, ensure a current CSRF token is available before submitting.
Shared Setup
Add an accessible status container after your form:
<div id="my-form-status" role="status" aria-live="polite"></div>
Add the following shared setup, then choose either the Plain JS or Axios request handler below.
const form = document.getElementById('my-form');
const status = document.getElementById('my-form-status');
let submitting = false;
// Complete the JavaScript Test when its generated input is present.
const javascriptTest = form.querySelector('[data-ff-check] + input[name]');
if (javascriptTest) {
javascriptTest.value = '';
javascriptTest.defaultValue = '';
}
async function submitForm(event, sendRequest) {
event.preventDefault();
if (submitting) return;
submitting = true;
status.textContent = 'Submitting…';
try {
const data = new FormData(form, event.submitter);
const response = await sendRequest(data);
// Preserve the updated payload for subsequent submissions.
const payload = form.querySelector('input[name="freeform_payload"]');
if (payload && response.freeform_payload) {
payload.value = response.freeform_payload;
payload.defaultValue = response.freeform_payload;
}
if (response.actions?.length || (response.success && !response.finished)) {
// Extend this example to handle payment actions or page changes.
status.textContent = 'Additional processing is required.';
console.log('Response requiring custom handling:', response);
} else if (response.success && response.finished) {
form.reset();
status.textContent = 'Form submitted successfully.';
} else {
status.textContent = 'Please review your entries and try again.';
console.log('Field errors:', response.errors || {});
console.log('Form errors:', response.formErrors || []);
}
} catch (error) {
status.textContent = 'Unable to complete the request. Please try again.';
console.error('Submission request failed:', error);
} finally {
submitting = false;
}
}
Replace my-form with your form's ID. Extend the validation response handling to display errors beside the appropriate fields.
Send the Request
After adding the Shared Setup, choose one of the following request handlers. Both use the same setup and response handling; only the method used to send the request differs.
- Plain JS
- Axios
Use the browser's built-in fetch API. No additional library is required.
form.addEventListener('submit', (event) => {
submitForm(event, async (data) => {
const result = await fetch(
form.getAttribute('action') || window.location.href,
{
method: 'POST',
body: data,
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
},
}
);
if (!result.ok) {
throw new Error(`Request failed with status ${result.status}`);
}
return result.json();
});
});
Use this option if your project already uses Axios. Ensure Axios is imported or loaded before this code runs.
form.addEventListener('submit', (event) => {
submitForm(event, async (data) => {
const result = await axios.post(
form.getAttribute('action') || window.location.href,
data,
{
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
},
}
);
return result.data;
});
});
When sending FormData, let the browser or Axios set the Content-Type header so the multipart boundary is included correctly.