# 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 ``` 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 `
` tag but linked with `form="form-id"` are included. Use bracket names (`tags[]`, `user[email]`) if your backend expects arrays or nested fields. ## Where error messages go By default submito appends a message to the field's parent element: ```html ``` - Wrap each control in its own element (a `