Render a form from its schema with one script tag, or drop in an iframe.
Script
<div data-quickforms="FORM_ID"></div>
<script src="https://YOUR-QUICKFORMS-HOST/embed.js" defer></script>The script finds every [data-quickforms] element, loads the form’s public schema and renders every field type with labels, help text, required markers and every honeypot, hidden. It posts with fetch, shows validation errors next to each field and the success message when it worked. Because it posts from your page, your site’s origin must be allowed by the form’s CORS settings.
If the form has Cloudflare Turnstile switched on, the script also loads https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit, renders the Turnstile widget with the form’s site key and sends its token as cf-turnstile-response. After a failed submission it resets the widget so the visitor can try again. Your site’s hostname must be on your Turnstile widget in Cloudflare. With data-sandbox, the widget is not rendered, because sandbox submissions skip Turnstile.
For containers added after page load, call window.quickforms.mount(element) or window.quickforms.boot().
When the uploads disk can sign uploads, the widget uploads each chosen file straight to storage first and sends the form as JSON with the upload IDs. Otherwise it sends file fields as multipart. For your own forms, window.quickforms.upload(file, options) uploads a file straight to storage and resolves with an upl_... ID to submit as the field’s value. See direct uploads.
Options
| Attribute | Description |
|---|---|
data-quickforms |
The form ID. Required. |
data-sandbox |
A sandbox key (qf_sandbox_...). The widget renders the form’s draft, so you can preview unpublished changes, submissions are sent in sandbox mode and a note says nothing will be delivered. |
data-submit-label |
Button text. Defaults to “Submit”. |
data-redirect |
Go to this URL after a successful submission. Must be http or https; anything else is ignored. |
data-unstyled |
Skip the default styles and style the qf-* classes yourself. |
Pin the script with SRI
/embed.js is the moving version: it is cached for 5 minutes and always has the latest fixes. To pin a release, use the versioned copy under /embed/v1/ with Subresource Integrity, so the browser refuses the file if it ever changes. Copies are immutable and cached for a year. You get new versions by updating the snippet.
The dashboard’s Embed script with SRI panel and the API’s embed.script_sri give you the exact tag:
<div data-quickforms="FORM_ID"></div>
<script src="https://YOUR-QUICKFORMS-HOST/embed/v1/embed.HASH.js" integrity="sha384-..." crossorigin="anonymous" defer></script>When the script is loaded from somewhere other than your quickforms host, such as a CDN, add data-host="https://YOUR-QUICKFORMS-HOST" to the tag so it knows where forms live. Snippets include this when QUICKFORMS_CDN_URL is set.
Direct uploads
window.quickforms.upload(file, { form, field, sandbox, host }) asks for a pre-signed upload URL, PUTs the file to it and resolves with the upload ID. Use it in a custom form that submits JSON.
const id = await window.quickforms.upload(input.files[0], { form: "FORM_ID", field: "cv" });
// Then submit JSON with { "cv": id, ... } to https://YOUR-QUICKFORMS-HOST/f/FORM_ID| Option | Description |
|---|---|
form |
The form ID. Required. |
field |
The file field’s name. Required. |
sandbox |
A sandbox key, sent as X-Quickforms-Sandbox. Needed for unpublished forms. |
host |
The quickforms host. Defaults to the host embed.js was loaded from. |
It rejects with the JSON error body if the upload URL can’t be issued, and with { message: "The upload failed. Please try again." } if the PUT fails. See file uploads for the full flow, limits and bucket CORS.
Theming
Set CSS variables on the container. These are the defaults:
[data-quickforms] {
--qf-accent: #0b0b0f; /* button and focus ring */
--qf-ink: #0b0b0f; /* text */
--qf-muted: #6b6b76; /* help text */
--qf-line: #dcdce0; /* input borders */
--qf-bg: #fff; /* input background, button text */
--qf-radius: 10px;
--qf-danger: #d93636; /* errors and required marker */
}With data-unstyled, target the classes directly: .qf (the form), .qf-field, .qf-label, .qf-input, .qf-help, .qf-error, .qf-invalid, .qf-button, .qf-status, .qf-status-ok, .qf-status-err and .qf-hp (the honeypot wrapper, keep it hidden) and .qf-turnstile (the Turnstile widget).
Events
The container dispatches quickforms:success and quickforms:error. event.detail is the JSON response body.
const el = document.querySelector('[data-quickforms]');
el.addEventListener('quickforms:success', (event) => {
console.log('Submitted', event.detail.id, event.detail.sandbox);
});
el.addEventListener('quickforms:error', (event) => {
console.log(event.detail.message, event.detail.errors);
});Iframe
Every form also has a hosted embed page at /f/{form}/embed that any site may frame. Add ?sandbox=qf_sandbox_... to preview the draft and send sandbox submissions from it.
The page uses the same script, so it renders the honeypots and the Turnstile widget too. The widget runs on the quickforms page rather than on your site, so if you use Turnstile, add your quickforms app’s hostname to your Turnstile widget in Cloudflare.
<iframe src="https://YOUR-QUICKFORMS-HOST/f/FORM_ID/embed" width="100%" height="600" style="border:0" title="Contact"></iframe>Public schema JSON
GET /f/{form}/schema.json returns what the widget renders from, cached for 60 seconds. Custom error messages are left out. Use it to build your own renderer.
| Key | Description |
|---|---|
honeypot |
The main honeypot name. |
honeypots |
Every honeypot name: the main one first, then any extra ones. Render them all, hidden. |
turnstile |
{"site_key": "..."} when the form has Turnstile switched on, otherwise null. Always null for sandbox previews. |
success_message |
The form’s success message, with any {{field_name}} placeholders unfilled. The submit response has the filled-in version. |
It returns the published version. Add ?sandbox=qf_sandbox_... to get the draft instead, sent with Cache-Control: no-store. A form that has never been published returns a 404 with “This form has not been published yet.” unless the sandbox key is given.
{
"id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"name": "Contact",
"action": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
"active": true,
"honeypot": "_gotcha",
"honeypots": ["_gotcha", "website", "fax"],
"turnstile": { "site_key": "YOUR_SITE_KEY" },
"success_message": "Thanks! Your submission has been received.",
"fields": [
{ "name": "email", "type": "email", "label": "Email", "required": true },
{ "name": "message", "type": "textarea", "label": "Message", "required": false, "max": 2000 }
]
}Filling in answers from the page address
Add data-prefill="url" to the embed’s container and any query parameter on the host page that matches a field name fills in that question. It’s off by default, because a parameter on your own site could share a name with a field.