Form 02
A contact form that reads as one sentence, "Hello, my name is ...
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialThe look of the preview is theform-02-*andform-set-*classes of the paste: the fields at rest and the button. The Embed holds what moves, and it reads its colours from five variables,--form-set-ink,--form-set-ground,--form-set-rule,--form-set-accentand--form-set-error: set them on the form, or on anything above it, in a rule of your own. The send button is a Link block, because a Webflow submit button is an input and cannot hold the layers that move; the script makes it send the form, and Enter in a field does too. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Use it on your own elements
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the element on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-form-set Marks the form. It takes no value: leave the value field empty. In the paste it is on the Form inside the Form block. The settings below go on the same element. data-form-set-field On each blank of the sentence, the box that holds one label and one control (a text, email or phone input, or a select). It takes no value. The engine writes the state on it and adds what it draws inside it. A control outside such a box is sent with the form but has no state and is not checked. [ Required ] data-form-set-field="check" On the label that holds a checkbox and its sentence. A required box left empty is refused with the words of the setting check. [ Optional ] data-form-set-submit On the send button: a submit button in plain HTML, a Link block in Webflow, which has no button that can hold layers. It takes no value. Without it the form is still checked when it is sent, by Enter for one, but nothing shows it leaving. [ Required ] data-form-set-hint On the line of help under a control. It takes no value. The reason takes its place while the field is wrong, then the hint comes back. A field without one gets a line from the engine, dressed like the first hint of the form. [ Optional ] data-form-set-fit On a field whose control is as wide as what is written in it, every blank of the sentence. It takes no value. [ Optional ] data-form-set-required="Your name, please." On one field: its own words for a reason, in place of the form's. The same goes for email, tel, short, pattern and check. [ Optional ] data-form-set-select="native" On one field: leaves its select to the list of the system, in place of the list the engine draws. On the form it is the setting select, for every select at once. A touch screen gets the system's picker either way, and the native select is what the form sends in both cases. [ Optional ] data-form-set-dialog Optional, for a form that opens in a dialog as Form 03 does: on a layer that holds a panel, the panel holding the form. With no value the panel rises in the middle of the window; with the value sheet, data-form-set-dialog="sheet", it slides in from the right. The stylesheet lays the layer over the window and hides it until it is opened. [ Optional ] data-form-set-open On what opens the dialog: any button or link, anywhere on the page, as many as you like. It takes no value, or the id of the dialog when the page has several: data-form-set-open="quote". [ Optional ] data-form-set-close On what closes the dialog, inside it. It takes no value, or cross to draw a cross in place of its word, which stays its name for a screen reader. Escape and a click outside the panel close it too, and so does the form, the time of the setting closeAfter after it was sent. [ Optional ] data-form-set-state Written by the engine. On a field: up, focus, filled, valid, error, disabled, several at once. On the send button: loading, done or failed. On the form: done. On a dialog: open. A field whose list is open also carries data-form-set-list="open". Never written by hand; the stylesheet reads them, and so can a rule of yours. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same element. An attribute always wins over the defaults written in the code.Attribute Default What it is data-form-set-send auto How the form leaves once every field is right. Options: auto lets a Webflow Form block send it to Webflow, and anywhere else posts the form to its action the usual way, the page changing; fetch posts the values to the action without leaving the page and reads the status of the answer. data-form-set-sending Sending The word on the button while the form is on its way. Options: any text, in the language of the page. data-form-set-sent Sent The word on the button once the form has left, beside the tick. Options: any text. data-form-set-failed Try again The word on the button when the send failed; the button can be pressed again. Options: any text. data-form-set-required This field is required. The reason under a required field left empty. Options: any text; data-form-set-required on one field gives it its own, "Your name, please.". data-form-set-email Enter a valid email address. The reason under a field of type email that does not hold an address. Options: any text. data-form-set-tel Enter a valid phone number. The reason under a field of type tel that does not hold a phone number, six digits at least. Options: any text. data-form-set-short A little more, please. The reason under a text shorter than the minlength of its control. Options: any text. data-form-set-pattern Check the format. The reason under a text the pattern of its control refuses. Options: any text; say the format expected, "Two letters, then four digits.". data-form-set-check Please tick this box. The reason under a required checkbox left empty. Options: any text. data-form-set-select drawn The list a select opens. Options: drawn, a list the engine draws under the control in the colours of the form, the native select staying underneath; native, the list of the system, which a touch screen gets either way. data-form-set-select="native" on one field leaves that select alone. data-form-set-close-after 2400 How long a dialog stays on the answer once its form is sent, in milliseconds. Only read by a form in a dialog. The way the form is sentNothing to connect: the paste is a native Webflow Form block, and Webflow receives what it sends. The entries are listed in Site settings, Forms, each field under the name of its label, and the address told of a new entry is set in the same place, under Form notifications. Entries only arrive from a published site, its webflow.io address included: the Designer and its preview send nothing. Once Webflow has the entry it hides the form and shows its Success message; when it could not, it shows its Error message and the button says Try again. Both are Webflow's own blocks, under the Form in the Navigator: select the Form block, open its settings and choose the state to see, restyle and rewrite each one. To add a field, duplicate a field block and give the new field its own name in its settings; to remove one, delete its block; Required is a switch in the same settings. Leave the Action of the form empty, or Webflow receives nothing. Only when the form is not a Webflow Form block do the attributes below apply.Custom attribute What it does action="https://..." On the form: the address the values are posted to, an endpoint of your own or one of a form service such as Formspree or Basin. Not a custom attribute in Webflow, where it is the Action field of the Form settings, to leave empty for Webflow to receive the entries. data-form-set-send="fetch" On the form: posts the values to the action without leaving the page, as FormData, with the header Accept: application/json. An answer in the 200s shows Sent, anything else Try again. It is the setting send, so it may also be set for the whole site. method="post" On the form: how the values are sent, POST when nothing is written. With GET and fetch they travel in the address.
Two files, no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
FormSet.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-form-set-send auto How the form leaves once every field is right. Options: auto lets a Webflow Form block send it to Webflow, and anywhere else posts the form to its action the usual way, the page changing; fetch posts the values to the action without leaving the page and reads the status of the answer. data-form-set-sending Sending The word on the button while the form is on its way. Options: any text, in the language of the page. data-form-set-sent Sent The word on the button once the form has left, beside the tick. Options: any text. data-form-set-failed Try again The word on the button when the send failed; the button can be pressed again. Options: any text. data-form-set-required This field is required. The reason under a required field left empty. Options: any text; data-form-set-required on one field gives it its own, "Your name, please.". data-form-set-email Enter a valid email address. The reason under a field of type email that does not hold an address. Options: any text. data-form-set-tel Enter a valid phone number. The reason under a field of type tel that does not hold a phone number, six digits at least. Options: any text. data-form-set-short A little more, please. The reason under a text shorter than the minlength of its control. Options: any text. data-form-set-pattern Check the format. The reason under a text the pattern of its control refuses. Options: any text; say the format expected, "Two letters, then four digits.". data-form-set-check Please tick this box. The reason under a required checkbox left empty. Options: any text. data-form-set-select drawn The list a select opens. Options: drawn, a list the engine draws under the control in the colours of the form, the native select staying underneath; native, the list of the system, which a touch screen gets either way. data-form-set-select="native" on one field leaves that select alone. data-form-set-close-after 2400 How long a dialog stays on the answer once its form is sent, in milliseconds. Only read by a form in a dialog. Closes sooner; 0 closes it at once Leaves more time to read the word on the button; off keeps it open until the visitor closes it The way the form is sentOnce every field is right the form leaves in one of four ways. With anactionand nothing else, the browser posts it and the page changes. Withdata-form-set-send="fetch"it is posted to that action without leaving the page, and the button says Sent or Try again from the status of the answer. With a listener onformset:submitthat callspreventDefault(), the page sends the values itself and ends withdetail.done(true)ordetail.done(false). Inside a Webflow Form block, Webflow sends it. Theaction="#"of the markup is a placeholder and sends nothing.Custom attribute What it does action="https://..." On the form: the address the values are posted to, an endpoint of your own or one of a form service such as Formspree or Basin. Not a custom attribute in Webflow, where it is the Action field of the Form settings, to leave empty for Webflow to receive the entries. data-form-set-send="fetch" On the form: posts the values to the action without leaving the page, as FormData, with the header Accept: application/json. An answer in the 200s shows Sent, anything else Try again. It is the setting send, so it may also be set for the whole site. method="post" On the form: how the values are sent, POST when nothing is written. With GET and fetch they travel in the address. -
Drive it from JavaScript
When the page sends the values itself, when it fills or checks the form from a script, or when it reacts to the form being sent.const element = document.querySelector('[data-form-set]'); element.addEventListener('formset:sent', (e) => { console.log(e.detail); }); // Send the values yourself: the button waits until done() is called. element.addEventListener('formset:submit', async (event) => { event.preventDefault(); const answer = await fetch('/api/contact', { method: 'POST', body: new FormData(event.detail.form) }); event.detail.done(answer.ok); // true shows Sent, false shows Try again }); const set = FormSet.mount(element); // the form auto() mounted, or a new one set.set('email', 'pam@dundermifflin.com'); // as if it had been typed set.validate(); // shows the reasons, returns true when the form can leave set.reset(); // empties it and takes every state off // When the form leaves the page: FormSet.destroy(element); // removes what the engine added, gives the markup back as it was
-
Fit it to your page
Sending the form
Once every field is right the form leaves, in one of four ways. The button shows
Sendingand a running bar meanwhile, thenSentwith a tick, orTry again.- In Webflow, as a native form. The paste is a real Webflow Form block: the Designer shows Form block, Form, the fields with their own settings, and the two message blocks. Nothing to connect. Webflow receives the entries itself: they are listed in Site settings, Forms, and on the site's Forms page, each field under the name of its label. The address that is told of a new entry, and the subject of that mail, are set in the same place, Site settings, Forms, Form notifications. Entries are only received on a published site, the webflow.io address included; the Designer and its preview send nothing. When Webflow has received the form it hides it and shows its Success message; when it could not, it shows its Error message under the form and the button says
Try again. Both messages are Webflow's own blocks, in the Navigator under the Form: select the Form block, open its settings and choose the state to see and restyle each one, and rewrite its words on the canvas. The engine knows a native form by its Form block, and reads which message Webflow shows. Do not give that form an Action in its settings: a Webflow form with an action is posted there instead, and Webflow receives nothing. - To an endpoint, without leaving the page. Give the form the address in its
actionand adddata-form-set-send="fetch". The values are posted withfetch, asFormData, with the form'smethod(POST when none is written) and the headerAccept: application/json, which is what Formspree, Basin, Getform and the like expect. An answer in the 200s showsSent; any other, or no answer, showsTry again. The endpoint has to accept requests from the page's origin. - The usual way. With an
actionand nothing else the browser posts the form and goes to the page the server answers with, as any form does. The states of the fields and the check before sending still play. - By hand. The form fires
formset:submitbefore it leaves, with{ form, done, reset }indetail. CallpreventDefault()on it, send the values yourself, thendetail.done(true)ordetail.done(false);detail.reset()empties the form to start again. This is how the preview works, and all its page adds to the two files: a listener ondocumentcallspreventDefault(), thendetail.done(true)after 1800 ms anddetail.reset()after 5200 ms. It sends nothing, holds the button, shows it sent, then empties the form. A page that must behave exactly as the preview carries that listener; a real page sends instead.
html <script> /* The preview sends nothing: it holds the form, shows it as sent, then empties it. */ document.addEventListener('formset:submit', function (event) { event.preventDefault(); setTimeout(function () { event.detail.done(true); }, 1800); setTimeout(function () { event.detail.reset(); }, 5200); }); </script>The snippet is written with
action="#", a placeholder that sends nothing: replace it with your address, or use one of the other ways. The Webflow paste leaves it out.Fields
- A field is one box marked
data-form-set-fieldthat holds onelabeland one control: aninputof type text, email, tel, number, url or password, aselect, or atextarea. Here every field is a blank of the sentence, see below. - To add a field, copy the box of a field of the same kind and give the new control its own
id, the same value in theforof its label, and its ownname: the name is what the entry is filed under. In Webflow, duplicate the field block in the Navigator and rename the field in its settings. - To remove a field, delete its box. To reorder, move the boxes: the engine reads them in the order of the page, and nothing else depends on it.
- What makes a field wrong is what its control says:
required,type="email",type="tel",minlength,pattern. Add or take off the attribute to change the rule. In Webflow, Required is a switch in the field's settings, andminlengthandpatternare custom attributes on the field. A field is checked when the visitor leaves it, then again at every key while it is wrong, and every field is checked when the form is sent: the first wrong one takes the focus. - A line of help goes in an element marked
data-form-set-hintinside the box. The reason takes its place while the field is wrong. - The checkbox is a
labelmarkeddata-form-set-field="check"that holds theinputand its sentence in aspan. - A select opens a list of its own, see below. It shows its first option while nothing is chosen: leave that option's value empty and write the invitation in it,
choose one.
The list of a select
A select opens a list drawn by the engine in place of the system's: a panel under the control, one line per option, the chosen one ticked.
- **The native
selectstays in the page**, under the list, and is what the form sends: itsname, itsvalue, itsrequired. The list is only a picture of its options, hidden from screen readers, which read the select itself as they always do. Add, remove or reorder theoptionelements and the list follows at the next mount; an option with an empty value, the invitation, and a disabled one are left out of it. - With the mouse, a press on the control opens the list, a click on a line chooses it, a click anywhere else closes it.
- With the keyboard, Enter, Space or an arrow opens it; the arrows, Home and End move the highlighted line and the value moves with it; Enter, Space or Tab keep the value, Escape puts back the one it had.
- On a touch screen (
pointer: coarse) the list is not drawn: the system's own picker opens, which is the better one under a thumb. - Its colours are the form's variables: the panel is
--form-set-ink, its words--form-set-ground, the tick of the chosen line--form-set-accent. For more, the list is.form-set-listand a line.form-set-option, with[data-form-set-active]on the highlighted line and[data-form-set-selected]on the chosen one: restyle them in a rule of your own, after the stylesheet. A list longer than its height scrolls. - To keep the system's list everywhere, set
selecttonative:data-form-set-select="native"on the form, or on the box of one field for that select alone. It is the choice for a very long list, a country for one, where the system's search by typing is worth more than the look. - While the list is open the field carries
data-form-set-list="open".
The words of the errors
Each reason is a setting, so an attribute on the form:
data-form-set-required="Ce champ est obligatoire.", and the same foremail,tel,short,patternandcheck. The same attribute on one field's box changes the words for that field alone. The words of the button aresending,sentandfailed. For every form of the site at once, change the settings block at the top of the Embed, or declarewindow.FormSetSettings = { required: '...' }before the script.The colours
Five custom properties, set at zero specificity on the form. Give them your own values on the form or on anything above it, the section or
body:--form-set-ink: the text of the fields, the line of the focus, the tick.--form-set-ground: the ground the form sits on; the tick of the checkbox is drawn in it.--form-set-rule: the rule under a field at rest.--form-set-accent: the plate and the bar of the button, and the line under the blank in focus.--form-set-error: a wrong field and its reason.
The look of the preview, the last part of the stylesheet and the classes of the Webflow paste, names its own colours: change them there too, in the Style panel or in that part of the file.
Writing your own sentence
The sentence is a row of boxes inside
form-02-sentence, with no white space between their tags, which is why each closing bracket of the markup sits at the start of the next line. Two kinds of box alternate:- a run of words,
<span class="form-02-words">and I work at</span>; - a blank,
<div class="form-02-blank" data-form-set-field data-form-set-fit>holding thelabeland the control, aninputwith a shortplaceholderor aselectwhose first option is the invitation.
Each box brings its own space after it, as a margin, so the words never touch a blank and nothing depends on the spaces of the markup: the Webflow paste has none. A full stop or a comma that follows a blank goes inside the blank, after the control, in
<span class="form-02-stop">.</span>, so it never starts a line. To write another sentence, keep the alternation and change the words, the controls and their placeholders; in Webflow, type in the Words blocks and duplicate a Blank. Each label is hidden from sight and read by a screen reader; it is also the name the entry is filed under. The sentence wraps like a text, blank by blank, down to a phone.In a dialog
Any form of the set can open in a dialog, the way Form 03 does. Three attributes, read by the same engine:
data-form-set-dialogon a layer that holds a panel, the panel holding the form. The stylesheet lays the layer over the page and hides it until it is opened; the layer and the panel take their look from classes of your own. With no value the panel rises into place, and comes up from the bottom edge on a phone: centre it in the layer. With the valuesheet,data-form-set-dialog="sheet", it slides in from the right, one second in and a little less out, and what it holds comes in after it: give the layerdisplay: flexandjustify-content: flex-end, and the panel amax-widthandmin-height: 100%.data-form-set-openon what opens it, any button or link of the page. With several dialogs on a page, give each layer anidand write it as the value:data-form-set-open="quote".data-form-set-closeon what closes it, inside the layer; the valuecrossdraws a cross in place of its word.
Escape and a click outside the panel close it too. While it is open the page behind does not scroll, Tab stays inside, and the focus goes to the first field, then back to what opened the dialog. Once the form is sent the dialog stays for
closeAftermilliseconds, then closes. Give the layerrole="dialog",aria-modal="true"and a name,aria-labelledbypointing at its heading. The spec of Form 03 has the whole of it.From JavaScript
FormSet.mount(form, overrides)returns the instance of a form, mounting it if it was not:{ element, settings, fields(), set(name, value), enter(name), leave(name), validate(), submit(), done(ok), reset(), refresh(), list(name, shown), open(), close(), destroy() }.setwrites a value as if it had been typed;validateshows the reasons and returns whether the form can leave;listshows the drawn list of a select, or closes it withfalse;openandcloseare for the dialog the form sits in, and do nothing for a form on the page.FormSet.auto(root)mounts every marked form insideroot, and runs once on its own;FormSet.destroy(form)gives the markup back, the dialog's too once its last form is gone.- Events, all bubbling from the form:
formset:ready,formset:invalid(with the wrong controls indetail.fields),formset:submit(cancelable, see above),formset:sentandformset:failed. - The state is on each element as
data-form-set-state:up,focus,filled,valid,error,disabledon a field, several at once;loading,doneorfailedon the button;doneon the form;openon a dialog. - **Under
prefers-reduced-motion: reduce** every state still shows, at once: the transitions and the bar of the button are cut to nothing. - Without JavaScript the form is a plain form: labels above their fields, the browser's own checks, a button that posts it.
- The words of the demo are the demo's: on a real page every label is the user's own, in the page's language, or asked for.
- In Webflow, as a native form. The paste is a real Webflow Form block: the Designer shows Form block, Form, the fields with their own settings, and the two message blocks. Nothing to connect. Webflow receives the entries itself: they are listed in Site settings, Forms, and on the site's Forms page, each field under the name of its label. The address that is told of a new entry, and the subject of that mail, are set in the same place, Site settings, Forms, Form notifications. Entries are only received on a published site, the webflow.io address included; the Designer and its preview send nothing. When Webflow has received the form it hides it and shows its Success message; when it could not, it shows its Error message under the form and the button says
-
Avoid the pitfalls
- **
action="#"sends nothing.** It is the placeholder of the snippet. A form outside a Webflow Form block needs a real address,fetchor not, or a listener onformset:submit. - In Webflow, entries only arrive from a published site. The Designer preview plays the states when custom code is enabled, and sends nothing.
- In Webflow the send button is a Link block, marked
data-form-set-submit. A native Webflow Submit button is aninput, which cannot hold the plate, the label and the bar. The script makes the Link send the form, and Enter in a field does too. A native Submit button marked the same way still sends, with no animation on it. - One look of fields per page. The forms of the set share the engine, and each stylesheet draws the fields its own way: Form 02 and Form 01 on one page would mix their looks. Two forms of the same number on a page are fine.
- The paste carries two Embeds. The first holds the settings and what moves, the second the engine. Keep both, in that order.
- **Every control needs a
name**, or its value is not sent, and alabel. - **A control outside a box marked
data-form-set-field** is sent with the form but is not checked and has no state. - A dialog laid over the page needs nothing above it that moves. The layer is
position: fixed: inside an element with atransform, afilteror aperspectiveit is fixed to that element instead of the window. Keep it out of such a box, at the end of the section or of the page. - The browser's own bubbles are off: the engine sets
novalidateon the form while it is mounted, and gives it back on destroy. - The stylesheet draws the states. Without
form-02.cssthe fields are checked but nothing shows, and the engine says so in the console. - **With
fetch, a server that redirects to a thank-you page** answers that page instead of a status: use the endpoint's JSON mode, which theAcceptheader asks for. - A blank needs room for its reason: the reason hangs under the blank, in the space between two lines of the sentence. Keep the line height of the sentence near twice its font size.
- **
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Form 02 is an animated form for Webflow and vanilla JavaScript, with no library. Also called natural language form, mad libs form or sentence form.
A contact form that reads as one sentence, "Hello, my name is ... and I work at ...", where every blank is a real field set in the line of text. A blank is as wide as what it says: it grows letter by letter as the visitor types, and a select takes the width of the choice made. On focus a thick line of the accent colour is drawn under the blank from the left; once the blank is right its rule goes and the answer reads as part of the sentence; a wrong one keeps a red line and its reason hangs just under it, and leaves at the keystroke that fixes it. The fields are checked as the visitor leaves them, with what the controls already say: required, an email, a phone number, a minimum length, a pattern. The send button has a plate of colour that rises on hover, shakes its head once when a blank is still wrong, runs a bar while the form leaves, then says Sent with a tick, or Try again. It is a real form: pasted in Webflow it is a native Form block whose entries land in the site's Forms tab; elsewhere it posts to its action, with fetch or the usual way, or hands the values to the page. Labels for screen readers, reasons read out, keyboard throughout, reduced motion respected. No library. Form 02 of the Form set. Pattern type natural language form, mad libs form, conversational contact form.
Updated
Questions
Paste the form: the sentence is a row of Words blocks and Blank blocks inside a native Webflow Form block. Each blank is marked data-form-set-field and data-form-set-fit and holds a real text field or select, so typing in the Words blocks and duplicating a Blank writes another sentence.
A field marked data-form-set-fit is as wide as what it says: the engine measures its value, or its placeholder while it is empty, on a hidden copy set in the same font, and a select takes the width of the choice made. No width is written in the stylesheet.
Yes. Every blank keeps its own label, hidden from sight and read out, the reason of a wrong blank is announced in a polite live region and tied to the field by aria-describedby, and Tab walks the blanks in the order of the sentence. Under reduced motion every state shows at once.
In Webflow it is a native Form block, so the entries land in Site settings, Forms, each under the hidden label of its blank. Elsewhere the form posts to its action, with data-form-set-send="fetch" to stay on the page, or the page sends the values itself from formset:submit.
Yes. The states read five custom properties, --form-set-ink, --form-set-ground, --form-set-rule, --form-set-accent and --form-set-error, set on the form or above it. The words are settings: data-form-set-required, data-form-set-email and the others, on the form or on a single blank.