Form web components
The published form is a pair of custom elements with no framework runtime and
no admin UI kit. The host page supplies a form id and an API origin. The same
script is what form-preview mounts inside the builder.
<script src="https://forms.example.com/form-embed.js" defer></script>
<link rel="stylesheet" href="https://forms.example.com/form-embed.css" />
<form-embed form-id="frm_123" api-base="https://forms.example.com/api/forms"></form-embed>
<form-embed-button
form-id="frm_123"
api-base="https://forms.example.com/api/forms"
button-text="Contact us"
></form-embed-button>
Prefix the tag names per product (spectre-form, and so on). The attributes
and the API below stay the same across hosts.
Elements
<form-embed>renders the form inline. Attributes:form-id(required),api-base,locale.<form-embed-button>renders a button that opens the same form in a dialog. Extra attributes are presentation only:button-text,button-background,button-text-color. The dialog contains one<form-embed>.
Resolve api-base in this order: the attribute, then a global the host page
sets (window.FORM_API_BASE), then same-origin /api/forms. A preview iframe,
a CMS page, and a storefront theme can each point at a different origin
without rebuilding the script.
form-id missing: render a short error inside the element and stop. Fetch
failure: the same, with a retry. Leave the host page alone.
API
Public, unauthenticated, CORS open to arbitrary embed origins.
| Call | Body | Returns |
|---|---|---|
GET {apiBase}/{id} | — | the published FormDefinition for the requested locale |
POST {apiBase}/{id}/submit | { fields, locale } | { ok: true } or { ok: false, fieldErrors } |
GET serves LIVE forms only. Drafts 404 here; the builder uses its own
preview read (see form-preview).
fields is a map of submission key → string, string array (multi-select,
checkboxes), or file id. The server checks required fields, option membership,
and conditional visibility (a hidden required field is not required). On
success it persists the submission, then hands off to form-integrations.
The response the element shows is successMessage from the definition.
Behavior inside the element
The element owns drawing: rows from the width packer, controls from each
field's control, conditional visibility, and the stepper when steps has
more than one entry. Previous / next move steps; submit exists on the last
step.
On submit, POST once, disable the button while in flight, and render either field errors beside their controls or the success message in place of the form. A second submit after success starts over only if the product wants that; the default is to leave the success state up.
File fields request POST {apiBase}/{id}/uploads for a presigned PUT, upload
the bytes to object storage, and submit the returned file id. The submit body
never contains the file.
View and interaction beacons (view, started, field_focus, step_change)
are optional POSTs to a sibling path. They never block render or submit.
Styles and build
The element carries its own stylesheet, class names prefixed, so the host page cannot restyle the form by accident and the form cannot leak into the host. Theme values from the definition (colors, radius, custom CSS) are applied as variables on the element.
Ship the script and CSS as build artifacts at a stable public URL. Edit the source and rebuild. A host that hotlinks the URL picks up the new renderer without a redeploy of the page.
The elements are the only public runtime. The builder stays in whatever framework the admin app uses.