Form builder
Describe a form in code. The builder outputs a valid FormDefinition for the API.
import { FormFlowClient, form } from "./sdk/typescript";
const def = form("Customer feedback")
.welcome({ title: "2 minutes, promise" })
.nps("nps")
.longText("why", "What is the main reason for your score?")
.choice("channel", "How did you find us?", ["Search", "Friend", "Social"], { multiple: true })
.email("email", "Email (optional)")
.theme({ colors: { primary: "#4f46e5" }, alignment: "center", radius: 16 })
.settings({ presentation: "conversational", notifications: { ownerEmails: ["team@example.com"] } })
.ending("Thank you!")
.build();
const ff = new FormFlowClient({ apiKey: process.env.FORMFLOW_API_KEY!, baseUrl: "https://YOUR_HOST" });
const f = await ff.forms.create({ name: "Feedback", definition: def });
await ff.forms.publish(f.id);Helpers
shortText, longText, email, phone, url, number, date, yesNo, choice (single, multiple, dropdown), rating, nps, csat, fileUpload, consent, statement, hidden, or field(type, id, label, props) for any type in the field catalog. startSection groups following fields into pages; logic adds rules; showWhen sets simple visibility.
Validation
The builder rejects invalid or duplicate ids immediately. References, cycles and reachability are checked by the form engine when you create or publish; a failed publish returns the issue list.
Design controls
Beyond colors and fonts, the definition carries optional design controls. Each is optional and unset means the renderer default, so send only what you want to change.
- Theme:
headingWeight(300-900),colors.heading,radii(button,choice,media,card, each falling back toradius),surface(kindnone|screen|section|field,shadow,color),choiceStyle(outline|raised|solid|radio|tile),progress(stylebar|segmented|hairline|none,thickness,color,track),numberStyle,requiredStyle,statementStyle,errorStyle,buttonWidth,logoSize,logoPlacement(addstop_right), andbackground.fit|position|imageOpacity.background.overlayOpacity: 0is honored. - Questions:
choiceLayout(stack|columns|hug|flow),columns,hideKeys,width(full|half|third|quarter|two-thirds|three-quarters, a 12-column grid that stacks on phones),placeholders,Choice.response; image choices addimageAspectandcardStyle; star ratings addicon(star|heart|thumb|circle|person|face) andshowNumbers; products addlayout. Media addsframe,fit,radius,ratio,mobileHeight,scrim,textTone. - Sections:
layout(default|group),collapsible,defaultOpen,tint,stickyHeader,card(fields in one card),columns(1-4),align(left|center|right). Welcome:hideTitle,inlineFirst. Endings:nextEndingId. - Settings:
navStyle(chevrons|pager|link|arrow|none),showTitle,showStepLabel,showEnterHint,showOkIcon,timeHint,confirmAnswers,sidePanel(withinsetandradiusfor art set in from the edges),header,siteHeaderandsiteFooter(logoUrl,logoAlt,text, up to 8linksoflabelandurl,align; shown on every screen).
{
"operations": [
{ "op": "set_theme", "theme": {
"colors": { "primary": "#0f766e", "background": "#fafaf9", "text": "#1c1917" },
"headingWeight": 500,
"surface": { "kind": "none" },
"choiceStyle": "tile",
"progress": { "style": "hairline", "thickness": 2 },
"radii": { "button": 999, "choice": 12 },
"buttonWidth": "full",
"logoPlacement": "top_right", "logoSize": 28
} },
{ "op": "update_field", "fieldId": "plan", "patch": {
"choiceLayout": "columns", "columns": 2, "hideKeys": true,
"choices": [
{ "id": "solo", "label": "Just me", "response": "Great, you can start free." },
{ "id": "team", "label": "My team" }
] } },
{ "op": "update_field", "fieldId": "first", "patch": { "width": "half" } },
{ "op": "update_section", "sectionId": "main", "patch": { "layout": "group" } },
{ "op": "set_settings", "settings": { "navStyle": "pager", "showEnterHint": false, "confirmAnswers": true,
"sidePanel": { "url": "https://example.com/side.jpg", "side": "left", "ratio": 40, "fit": "cover", "mobile": "banner" } } }
]
}Part styles
theme.parts restyles one named part of the form at a time, so a competitor look is settings rather than code. Parts: page, stage, screen, card, fieldCard, header, title, description, sectionTitle, sectionDescription, questionTitle, questionDescription, number, required, input, placeholder, select, choice, choiceSelected, choiceLabel, choiceIndicator, scaleCell, scaleCellSelected, link, badge, button, buttonSecondary, progress, progressTrack, error, endingTitle, endingDescription, siteHeader, siteFooter, brand, logo, divider, image. Part styles win over presets and tokens.
- Colour and background:
color,background(colour or plain gradient, repeating ones too),backgroundLayers(up to 3 gradients),backgroundImage(urlhttps, an app path or a base64 image;size,position,repeat) andtextFill(gradient text with a colourfallback). - Borders and outline:
borderWidth,borderStyle(solid|dashed|dotted|double|groove|ridge|inset|outset|none) andborderColor, each also per side (borderTopWidth,borderTopStyle,borderTopColor...),outlineWidth,outlineStyle,outlineColor,outlineOffset;radiusand per-corner radii in px (0-999) or a percentage string up to 50%. - Spacing and size (px unless noted):
paddingX,paddingY,paddingTop/paddingRight/paddingBottom/paddingLeft(0-200, a side wins),marginTop/marginBottom/marginLeft/marginRight(-200 to 200),width,maxWidth,minWidth,height,maxHeight(px or a percentage string),minHeight,aspectRatio,objectFit(image part),display(block|inline-block|flex|none),flexDirection,alignItems,justifyContent(container parts),gap. - Text:
fontFamily(Google families load with fontSource google,customFontsfamilies always),fontSize,fontWeight,fontStyle(incl. oblique),fontVariant,letterSpacing,wordSpacing,lineHeight(ratio or a px string),textTransform,textAlign(incl. justify),textDecoration(underline|line-through|overline) withtextDecorationColor,textDecorationThickness,textUnderlineOffset,whiteSpace,textOverflow,textShadow(up to 2). - Effects:
shadow(none|sm|md|lg|xl, an object with x, y, blur, spread, color, inset, or a list of up to 4),opacity,backdropBlur,blur,brightness,grayscale,transform(rotate, scale, translateX, translateY, skewX),transition(duration, easing),mixBlendMode. - States:
hover,active(pressed) anddisabledtake colours, borders, outline, shadow, transform and opacity;focustakes the same plusringColorandringWidth;mobilerepeats the base properties for phones. Theplaceholderpart styles input placeholder text. - A section or a field takes its own
stylewith the same shape, limited to the parts inside it, and it wins over the theme. A section title placed above its card is reached throughsection.style.sectionTitle.
{
"operations": [
{ "op": "set_theme", "theme": {
"colors": { "primary": "#673ab7", "background": "#f0ebf8", "text": "#202124" },
"parts": {
"stage": { "maxWidth": 640 },
"header": { "background": "#ffffff", "borderTopWidth": 10, "borderColor": "#673ab7", "radius": 8, "paddingX": 24, "paddingY": 22 },
"fieldCard": { "background": "#ffffff", "borderWidth": 1, "borderColor": "#dadce0", "radius": 8, "paddingX": 24, "paddingY": 24, "marginBottom": 12 },
"input": { "background": "transparent", "borderBottomWidth": 1, "radius": 0, "focus": { "borderColor": "#673ab7" } },
"button": { "background": "#673ab7", "radius": 4, "hover": { "background": "#5e35b1" } }
} } },
{ "op": "update_section", "sectionId": "main", "patch": { "columns": 2, "card": true, "style": { "sectionTitle": { "fontSize": 20 } } } },
{ "op": "update_field", "fieldId": "email", "patch": { "width": "two-thirds", "style": { "input": { "borderColor": "#d93025" } } } },
{ "op": "set_settings", "settings": {
"siteHeader": { "text": "Acme Co", "links": [{ "label": "Help", "url": "https://example.com/help" }] },
"siteFooter": { "text": "Privacy first", "align": "center" } } }
]
}Field options
fill_blankblanks[](up to 30): each isid,answer,width(2-60 characters),boxWidth(16-800 px, the blank's own width),marginLeft(-100 to 400 px of room before the blank),placeholder(max 100),caption(max 60, a line under the blank, styled by theblankCaptionpart) andoptions(1-50 strings; the blank becomes a select). The label holds the blanks as___; the answer is an object keyed by blank id.fillOrderon a columns choice:across(default, row by row),down(each column top to bottom, as printed surveys do) orpacked(down, but each column stacks on its own and an option spanning every column starts a new band).short_text:entry: "comb"draws one box per character withcombCount(2-24, default 6); the answer stays one string.fit: "answer"sizes the box to the longest allowed answer.long_text.ruled:{ color, lineHeight }(hex colour, 16-60 px) draws lined paper that scrolls with the text.simple:{ label, helpText }(max 2000 each), a plain-language twin shown when the respondent turns on simple wording in the comfort panel.confidence:{ label, min, max }adds a 0-100 slider under the answer; the value is stored as the answer keyfieldId.confidence.sticky(boolean): on page layouts the field stays in view while the rest scrolls; ignored in one-question layouts.- Canvas layout:
settings.presentation: "canvas"shows questions as cards on a pannable, zoomable 2D board; a field'scanvas: { x, y }(px, 0-10000) places its card, otherwise cards are placed automatically. Jump logic is drawn as arrows. Phones use the standard page layout.
{ "op": "update_field", "fieldId": "code", "patch": { "entry": "comb", "combCount": 8, "simple": { "label": "Your 8 digit code" }, "confidence": { "label": "How sure?", "min": "Guess", "max": "Certain" } } }Settings reference
settings.anchorLine(0.1-0.9): the active question lands with its top at this fraction of the viewport height when it changes. Default no scrolling.settings.comfort:offer(1-6 unique of text-size, readable-font, contrast, reduce-motion, read-aloud, simple-wording) andposition(bottom-left|bottom-right|top-left|top-right); a button opens switches the respondent keeps per browser.settings.answerTrail(top|side): chips of the answered questions, each jumping back to its question.settings.feedback:{ haptics, sound }booleans; a short vibration and a soft click on choosing and submitting, off under reduced motion.settings.history(boolean): an Undo button and a list to restore from, up to 50 earlier answer states.settings.constraints(up to 40):{ item, edge, to, toEdge, offset }pins an item (a pin or region item id) edge to an edge of title, submit, progress, logo, card, stage, header, actions orfield:id[.label|.input]. Edges are left|right|top|bottom|centerX|centerY and must be on the same axis;offsetis -1000 to 1000 px. Needs CSS anchor positioning.settings.sentenceMode:{ text }(max 4000) turns a page layout into one paragraph; each{fieldId}becomes that field inline (an input, or a select for a choice).settings.actions.extra(up to 4):{ id, label, kind, url, style, partStyle }with kind link|reset|print;partStyleis that button's own part style over theactionButtonpart.- Region items (
settings.regions):imageUrldraws an image before the text of a badge, link, button or icon;imageAfterputs it after;imageWidth(8-400 px, the height isiconSize);justify(start|center|end) aligns the content across the item width. - Parts
hero(the header banner or illustration) andblankCaption(the caption under a fill-in blank) take the normal part style properties.
Theme additions
theme.progressTint:{ from, to, target }(hex colours; target background|accent, default accent) moves the colour fromfromtotoas progress runs 0 to 100%.theme.schedule:{ darkFrom, darkTo }(HH:MM, may cross midnight) uses the dark palette in the respondent's local time between the two. No location needed.theme.entrance:title(fade|rise|type|none),choicesandbutton(fade|rise|none),stagger(0-300 ms between choices) anddelay(0-1000 ms); all optional, off under reduced motion.theme.density(auto|compact|comfortable): spacing by pace; auto watches the respondent's own timing in the browser and sends nothing.themeRules(form level, up to 20):{ when, theme };whenis a logic condition andthemea strict partial limited tocolors,background,logoUrl,buttonStyleandparts. Every rule that holds is merged over the base theme in order, later wins.layoutRules(form level, up to 20):{ when, layout }wherelayoutis anysettings.presentationvalue; the first rule that holds picks the layout, answers are kept.- Part style
when:focused,filled,empty,invalid,prefilled,locked, each a part style applied while the field is in that state. Part styleat: a list of{ min, max, style }(up to 4; px 200-2000, one bound is enough) measured against the form column, not the window;stylemay not holdwhenorat. - Part style
fontVariation:wght100-1000,wdth25-200,opsz6-144,slnt-20 to 0 (variable font axes; a font without an axis ignores it).alignSelf(start|center|end|stretch) places any part across its own row.
{ "op": "set_theme", "theme": {
"progressTint": { "from": "#dc2626", "to": "#16a34a" },
"schedule": { "darkFrom": "20:00", "darkTo": "06:30" },
"entrance": { "title": "type", "choices": "rise", "stagger": 60 },
"density": "auto",
"parts": { "questionTitle": {
"when": { "focused": { "fontVariation": { "wght": 700 } } },
"at": [{ "max": 480, "style": { "fontSize": 20 } }] } } } }Batch edits
await ff.forms.edit(f.id, () => [
{ op: "add_field", field: { id: "company", type: "company", label: "Company" }, index: 1 },
{ op: "set_theme", theme: { radius: 20 } },
]);