# Submito documentation
> Vanilla JavaScript AJAX forms: live validation, captchas, phone input and response dialogs. Zero dependencies.
Submito is a dependency-free browser library. `submito.newForm({ form: '#id' })` binds an existing HTML form to `fetch`: validation from HTML constraints (live while typing), a built-in response dialog (or SweetAlert2), math captcha, Google reCAPTCHA v3, Cloudflare Turnstile, intl-tel-input phone fields, and bubbling `submito:*` events on the form element. Endpoints reply with JSON such as `{ "status": "success", "message": "…" }`, or HTTP 422 with `{ "message": "…", "errors": [{ "name": "email", "message": "…" }] }`.
The npm package ships TypeScript types (`types/index.d.ts`) and an agent skill for Claude Code, Cursor and Codex (`npx submito skill`). Every page below is plain Markdown.
---
# Getting started
Submito binds an existing HTML form to `fetch`. You keep your markup and CSS. submito validates the fields, sends the form as `FormData`, and shows the server's reply. It runs in the browser only.
## 1. Load the library
### Script tag
```html
```
This exposes `window.submito`. You can also self-host the files from `dist/` after `npm run build`. `submito.js` is the readable build and `submito.min.js` is the minified one.
### npm
```bash
npm install submito
```
```js
import submito from 'submito';
```
TypeScript types are included, so editors autocomplete every option, event and response field.
:::tip Using an AI coding agent?
Run `npx submito skill` to give Claude Code, Cursor or Codex the submito skill. See [AI & agents](/ai.md).
:::
## 2. Write a normal form
```html
```
- Every field you want to send needs a `name`.
- Use normal HTML constraints (`required`, `type="email"`, `minlength`…). submito reads them, so you do not configure rules twice.
- `action` and `method` on the form are used as defaults. The form also still works without JavaScript.
## 3. Bind it
```js
submito.newForm({
form: '#contact-form',
indicator: '#sending',
});
```
That is all. On submit, submito:
1. Validates every field and shows inline errors (no request is sent if something is invalid).
2. Disables the submit button, shows `#sending`, and sets `aria-busy` on the form.
3. Sends `FormData` to `action` with `Accept: application/json`.
4. Reads the JSON reply. On success it shows a dialog and resets the form. On error it shows server field errors inline and the message in a dialog.
`form` accepts an element, an id (`'contact-form'`), or a selector (`'#contact-form'`). If the form is missing, or there is no `url` / `action`, `newForm` logs a `console.error` and returns `null`.
## 4. Return JSON from your server
```json
{ "status": "success", "message": "Thanks, we will get back to you soon." }
```
```json
{
"status": "error",
"message": "Please fix the highlighted fields.",
"errors": [{ "name": "email", "message": "This email is already registered." }]
}
```
The HTTP status code counts too: any 4xx or 5xx response is treated as an error. See [Server responses](/server-responses.md) for every accepted shape, including Laravel and WordPress.
## Handle the response yourself
Pass `callback` to replace the built-in UI. Events still fire.
```js
submito.newForm({
form: '#contact-form',
callback(response, form) {
if (response.success) {
showToast(response.message);
form.reset();
} else {
form.showErrors(response.errors);
}
},
});
```
## Next
- [Configuration](/configuration.md): every option
- [Validation](/validation.md): rules, live validation, custom messages
- [Response UI](/response-ui.md): dialog, SweetAlert2 or none
- [Events & instance API](/events.md): hooks for CSRF tokens, analytics, cancelling
- [Playground](/playground/): try everything live
---
# Configuration
```js
const form = submito.newForm(options);
```
`newForm` returns a form instance (see [Events & instance API](/events.md)). It returns `null` and logs a `console.error` when the form or endpoint cannot be found. If you call it again for the same form, the previous instance is destroyed first.
## Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `form` | `string \| HTMLFormElement` | — | **Required.** The form element, its id (`'contact'`), or a selector (`'#contact'`). |
| `url` | `string` | form `action` | Endpoint passed to `fetch`. Relative URLs resolve against the page. |
| `method` | `string` | form `method`, else `'POST'` | HTTP method. `GET` / `HEAD` send fields as a query string with no body. |
| `submitButton` | `string \| Element \| Element[]` | — | Extra trigger(s), e.g. a button outside the form. These are disabled during requests, like the form's own submit buttons. |
| `indicator` | `string \| Element` | — | Loading element, shown while a request is running. |
| `data` | `object \| (form) => object` | — | Extra fields added on every submit. A function runs per submit and may be `async`. |
| `headers` | `object \| Headers \| (form) => object` | — | Extra request headers, merged over `Accept: application/json`. |
| `responseType` | `'dialog' \| 'swal' \| 'none'` | `'dialog'` | Built-in popup for responses. See [Response UI](/response-ui.md). |
| `liveValidation` | `boolean` | `true` | Validate while the user types. `false` validates on submit only. |
| `resetOnSuccess` | `boolean` | `true` | Reset the form after a successful response (built-in UI only). |
| `messages` | `object` | — | Override built-in strings, e.g. `{ required: 'Required' }`. See [Internationalization](/internationalization.md). |
| `swal` | `object` | — | Extra SweetAlert2 options when `responseType: 'swal'`. |
| `recaptcha` | `string \| object` | — | reCAPTCHA v3 site key or `{ siteKey, action, field, enterprise }`. See [reCAPTCHA](/recaptcha.md). |
| `turnstile` | `true \| object` | — | Cloudflare Turnstile. See [Turnstile](/turnstile.md). |
| `iti` | `true \| object` | — | intl-tel-input phone field. See [Phone numbers](/intl-tel-input.md). |
| `callback` | `(response, form) => void` | — | Replaces the built-in response UI. Can be `async`. |
The [math captcha](/math-captcha.md) needs no option: add an input named `numb_captcha`.
## Full example
```js
submito.newForm({
form: '#contact-form',
url: '/api/contact',
method: 'POST',
indicator: '#sending',
data: () => ({ page: location.pathname }),
headers: () => ({ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content }),
responseType: 'dialog',
messages: { success: 'Thanks! We will reply within a day.' },
turnstile: { siteKey: 'YOUR_TURNSTILE_SITE_KEY' },
iti: { input: '#phone', country: 'US' },
});
```
## `data`: extra fields
```js
// Same values on every submit
data: { source: 'homepage', campaign: 'spring' }
// Computed per submit (may return a Promise)
data: (form) => ({ submitted_at: new Date().toISOString() })
```
- Values **replace** form fields with the same name.
- Arrays become repeated entries (`tags: ['a', 'b']` → `tags=a&tags=b`).
- Plain objects are sent as JSON strings. `File` / `Blob` values are sent as files.
- `null` / `undefined` values are skipped.
For one-off changes you can also edit `event.detail.formData` in a [`submito:before`](/events.md) listener.
## `headers`
```js
headers: { 'X-Requested-With': 'XMLHttpRequest' }
```
submito always sends `Accept: application/json` (frameworks such as Laravel use it to return JSON validation errors). Do not set `Content-Type` yourself: the browser adds the multipart boundary.
## `submitButton`
You usually do not need it. Every submit button of the form already works, including Enter in a text field and buttons outside the form linked with `form="id"`. Use it for elements that are not submit buttons:
```html
Subscribe
```
```js
submito.newForm({ form: '#newsletter', submitButton: '#subscribe-link' });
```
## `indicator`
```html
Sending…
```
While a request runs, submito removes the hiding marker the element already uses: the `hidden` class (Tailwind), `uk-hidden` (UIkit) or `d-none` (Bootstrap). It adds the marker back afterwards. If the element has none of these, the `hidden` attribute is used, and the element is hidden on init.
The submit buttons are disabled for the same period, and the form gets `aria-busy="true"`. Everything is restored when the request ends, including after errors and cancelled submits. A second submit while one is running is ignored.
---
# Forms & markup
submito works with the form you already have. It reads values, writes error text, and toggles a few attributes. You style everything.
## What submito changes on the form
| When | Change |
|------|--------|
| `newForm()` | `novalidate` on the form (browser bubbles are replaced by inline messages). Restored by `destroy()`. |
| Field invalid | `aria-invalid="true"` + `aria-describedby` on the control, class `error` on its wrapper, a message element. |
| Request running | `aria-busy="true"` on the form, submit buttons `disabled`, indicator shown. |
| Success (built-in UI) | `form.reset()`, unless `resetOnSuccess: false`. |
## Fields
Every field you want to send needs a `name`. submito builds the payload with the browser's own `new FormData(form)`, so the rules are the native ones:
| Control | Sent |
|---------|------|
| text, email, tel, number, date, hidden, … | the `value` |
| `select` | the selected value(s); `multiple` sends one entry per option |
| `textarea` | the value |
| checkbox / radio | only when checked |
| `file` | each selected file; empty file inputs are dropped |
| the clicked submit button | its `name=value`, if it has a `name` |
| disabled fields | nothing |
Fields outside the `