Forms developer docs

Embed a form on your website

One script tag and one element. This page is the whole contract: the five modes, every attribute with its default, the events your page can listen for, and the things the embed cannot do.

The two lines

Paste the element where you want the form, and the script tag once anywhere on the page. The script is served from the same origin as the form itself, so a republish never asks you to paste anything again.

<div data-faeform="FORM_ID" data-faeform-auto-resize></div>
<script src="https://forms.faedoc.com/embed.js" async></script>

The script finds every element that carries one of the mode attributes below, marks it with data-faeform-ready so a second run cannot mount it twice, and builds the form. One script tag is enough for every form on the page.

The five modes

Each mode is one attribute. A data-faeform element can name a mode with data-faeform-mode instead, which is what a snippet that only ever changes one word needs. A missing or unknown mode is inline, which is what every older snippet meant.

Modes, the attribute that selects each one, and when the form appears.
ModeAttributeWhat appearsOpens when
inlinedata-faeformThe form fills the element you put on the page.On page load. There is nothing to click.
popupdata-faeform-popupA centred card over a dimmed page.A click on the element that carries the attribute.
sliderdata-faeform-sliderA full height panel from the right, or from the left with data-faeform-side.A click on the element that carries the attribute.
sidetabdata-faeform-sidetabA tab fixed to one edge of the window, plus the same panel the slider uses.A click on the tab. A second click puts the panel away.
popoverdata-faeform-popoverA round floating button, plus a small card beside it. The page behind stays usable.A click on the button. A second click puts the card away.

Copy and paste, one per mode

These are the snippets the Share panel in your dashboard produces, with your form id in place of FORM_ID. Nothing else on this page changes what they do.

On the page (inline)

<div data-faeform="FORM_ID" data-faeform-auto-resize></div>
<script src="https://forms.faedoc.com/embed.js" async></script>

Popup on a button (popup)

<button type="button" data-faeform-popup="FORM_ID">Open the form</button>
<script src="https://forms.faedoc.com/embed.js" async></script>

Side panel on a button (slider)

<button type="button" data-faeform-slider="FORM_ID">Open the form</button>
<script src="https://forms.faedoc.com/embed.js" async></script>

Tab on the edge (sidetab)

<div data-faeform-sidetab="FORM_ID" data-faeform-label="Open the form"></div>
<script src="https://forms.faedoc.com/embed.js" async></script>

Floating button (popover)

<div data-faeform-popover="FORM_ID" data-faeform-label="Open the form"></div>
<script src="https://forms.faedoc.com/embed.js" async></script>

Attributes and defaults

Every attribute is optional. A value that is not a number, or a number outside the range, is replaced by the default rather than refused, so a typo in an attribute can never leave a blank space where the form should be.

Every optional attribute embed.js reads, what it applies to, and what it does when you leave it out.
AttributeApplies toValueDefault
data-faeform-modeA data-faeform element only. Ignored on an element that already names its mode.inline, popup, slider, sidetab or popover.inline
data-faeform-sideslider, sidetab, popoverleft or right.right
data-faeform-heightinlineFrame height in px, 1 to 20000.520
data-faeform-widthslider, sidetabPanel width in px, 280 to 1200. A window under 640px wide ignores it and goes full width.480
data-faeform-auto-resizeinlinePresent or absent. Present means the frame follows the form content. The height above becomes a floor, never a ceiling.absent, so the frame keeps one height
data-faeform-titleevery modeThe accessible name of the frame, and the label of an overlay dialog.Form
data-faeform-labelsidetab, popoverThe words on the tab or the floating button.Open form
data-faeform-hiddenevery modeURL parameters as source=web&campaign=spring. They ride on the # fragment, so they never reach a server log.none
data-faeform-open-delaypopup, slider, sidetab, popoverSeconds after load before the form opens itself, 1 to 600.0, which means never
data-faeform-open-scrollpopup, slider, sidetab, popoverPercent of the page read before the form opens itself, 1 to 100.0, which means never
data-faeform-cappopup, slider, sidetab, popoverHow many times a trigger may open this form on one browser, 1 to 100.0, which means no cap and nothing written to storage
data-faeform-cap-dayspopup, slider, sidetab, popoverHow many days that count runs for, 1 to 3650.30

Panel sizes give way to the window. Under 640px wide, every overlay goes full width and full height, and data-faeform-width is ignored.

Hidden values ride on the # fragment of the frame URL. A fragment is never sent to a server, so a value you put there does not reach our request logs or your own.

What your page can hear

Every event travels one way, from the frame to your page. There is no message your page can send into the frame, so nothing here lets you skip a question, prefill an answer or submit a form from the outside.

The version 1 event table. Each event reaches your code through the callback named beside it.
EventCallbackPayloadWhen it fires
readyonReadynoneAfter the form runtime mounts inside the frame. It is the first message, and it is what tells the script the frame is alive.
startedonStartednoneOnce a session, when the respondent first acts on a question: leaving the welcome screen, or the first answer on a form with no welcome screen.
question_changedonQuestionChanged{ ref }, the question reference nameWhen the question in front of the respondent changes. Never while the welcome screen is up.
height_changedonHeightChanged{ height }, whole pixels, never above 20000On mount and on every content resize.
submitonSubmit{ ending }, the ending reference name or nullWhen the answers are accepted and an ending screen appears. Once per completed response.
ending_button_clickonEndingButtonClick{ ending }, the ending reference name or nullWhen the respondent clicks the button on the ending screen, before the host page follows any link.
closeonClose{ reason }The respondent asked to close the form, normally with Escape inside the frame. It is a request to the host page, not a statement that anything closed.
erroronError{ code, message }The form cannot go on. The code is submit_failed or form_closed, and load_failed is added by the script itself.

onClose is given one of api, button, backdrop, escape, frame, pagehide. The frame reason means the respondent pressed Escape inside the form, and pagehide means the browser is putting the page away.

Errors

The message text is looked up on your page from a fixed table. It never comes out of the frame, so a server message can never be printed on your site through a callback.

The three error codes a callback can receive.
CodeMessageRaised by
submit_failedThe answers could not be sent.The form, after a failed send.
form_closedThis form is no longer accepting answers.The form, when the owner has closed it.
load_failedThe form could not be loaded.embed.js on the host page, when the frame never reports ready.

Listening

Attributes cannot carry a callback, so a page that wants the events calls the script API instead. The options are the same ones the attributes set, written as an object.

<div id="quote-form"></div>
<script src="https://forms.faedoc.com/embed.js"></script>
<script>
  FAEForm.load(document.getElementById("quote-form"), "FORM_ID", {
    autoResize: true,
    height: 520,
    hidden: { source: "web", campaign: "spring" },
    onReady: function () { /* the frame is alive */ },
    onStarted: function () { /* the respondent answered something */ },
    onQuestionChanged: function (event) { /* event.ref */ },
    onSubmit: function (event) { /* event.ending */ },
    onError: function (event) { /* event.code, event.message */ }
  });
</script>
FAEForm.load(element, formId, options)   // inline, returns the iframe
FAEForm.open(formId, options)            // popup,   returns { close }
FAEForm.slider(formId, options)          // slider,  returns { close }
FAEForm.popover(formId, options)         // returns { open, toggle, close }
FAEForm.sidetab(formId, options)         // returns { open, toggle, close }
FAEForm.version                          // 1

What the events do and do not reveal

No event carries a respondent's answer, and no event carries a hidden value. The payloads are heights, question reference names, ending reference names and a fixed error code.

That is not the same as saying the events tell you nothing about a person. question_changed arrives in the order the respondent walked the form, and submit names the ending they reached, so the ordered list of refs plus the ending ref describes the branch a respondent took. On a form whose branches are meaningful, for example one where a question is only asked after a particular answer, the path is close to the answer.

Any script running on your page can read these events. The frame posts them to the page window, and our script filters by origin and by frame before it hands one to your callback, but a listener somebody else put on your page has no obligation to do the same. If your site loads tag managers, analytics or advertising scripts, treat the branch path as something those scripts can see.

Opening the form by itself

An overlay mode can open with no click, on a delay or at a scroll depth. Nothing here runs unless you set the attribute. Whichever trigger fires first disarms the other, and a page going into the back and forward cache disarms both so a restored page does not open a form the moment it comes back.

  • data-faeform-open-delay counts seconds from page load.
  • data-faeform-open-scroll counts percent read. A page shorter than the window reads as 100 percent, so a scroll trigger on a short page fires at once.
  • A click never counts against the cap. Somebody who clicks asked for the form.
  • A trigger that fires while the form is already open does not count either, because it showed the reader nothing new.

The frequency cap, and the one thing it stores

With data-faeform-cap set, the browser keeps a count and a timestamp under faeform:cap: plus the form id. That is the whole record: no answer, no hidden value, no identifier, no visit history, and nothing sent anywhere. It exists to show the form less often. A page that sets no cap attribute writes nothing at all, and a browser that refuses storage, which is what private mode does, simply has no cap.

The key is named after the FORM, not after the element. Two embeds of the same form on one site, say a popup on the pricing page and a side tab everywhere else, share one counter. A cap of one is one opening across both, not one each. Two different forms have two separate counters.

An automatic opening on a phone is your page's risk to weigh, not ours. A form that covers the content on a small screen is the shape search engines describe as an intrusive interstitial, and it is your page that would be judged for it. If you use a delay or scroll trigger, keep the cap low and consider leaving the trigger off for narrow windows.

Overlays, focus and scrolling

Every overlay is a dialog with an aria-modal label, a close button that takes focus when it opens, Escape to close, and focus returned to whatever opened it. Two overlays can be open together, and only the newest answers the keyboard, so one Escape closes one form.

The popup, slider, sidetab modes hold the page still while they are open by setting the body overflow. Popover does not, because it floats beside the page rather than over it.

That hold does not work on iOS Safari. Setting body overflow to hidden does not stop the page behind from scrolling there, so a respondent on an iPhone can scroll your page under an open form. It is a browser behaviour rather than a setting, and no attribute on this page changes it. If a page behind a form must not move on iOS, that is a decision for your own CSS.

Tab is kept inside the panel while focus is on your page. Once the respondent tabs into the frame, the form owns focus and keeps it there, which is a browser rule rather than a gap.

A reader who has asked their system for less movement gets no entry animation. The overlay is built in place.

When the frame does not load

If no message arrives from the frame within 12 seconds, the script puts a plain visible link beside it: The form did not load on this page, and Open the form in a new tab. Your hidden values are carried on that link. The onError callback is called with load_failed at the same moment.

A frame that fired its own load event but has not reported ready gets one more 12 second window before that happens, because a slow phone and a blocked frame look identical at the deadline and only one of them is worth waiting for.

Inline leaves the frame where it is and adds the link underneath, in case the frame is only slow. An overlay panel clips what it holds, so there the frame is taken out of the layout first and the link takes the space.

Versions and version skew

Every message names a protocol version, and it is 1 today. FAEForm.version reports the same number. The version is bumped only for a breaking change: a new event type does not need one, so a page that ignores event names it does not know keeps working.

The script and the form page are cached separately, so an old script can meet a new frame for one page load. The four events that existed before versions were added are therefore posted twice, once in the current shape and once in the old one. An old script does not recognise the current name and ignores it; a current script ignores the old copy as soon as it has seen any versioned message.

The four events that carry a version 0 copy, and the old name on that copy.
Current nameVersion 0 name
readyready
height_changedheight
question_changedquestion-changed
submitsubmit

started, ending_button_click, close and error are new in version 1 and have no old copy, because no old script ever asked for them.

Related

To read forms, revisions and responses from your own server, see the Forms developer API. To have each response pushed to you as it arrives, see the setup guides for Zapier, Make and n8n.