Skip to content

FormDSL Reference

Download as Markdown

For LLMs generating FormDSL forms. This document covers the complete JSX DSL surface area for both SolidJS (@formdsl/solid) and React (@formdsl/react). The APIs are identical across frameworks.

Quick Start

tsx
// SolidJS
import { Form, Section, Question, Row } from '@formdsl/solid'
// React
import { Form, Section, Question, Row } from '@formdsl/react'

export default function MyForm() {
  return (
    <Form id="my-form" version="1.0" onSubmit={(answers) => console.log(answers)}>
      <Section title="Contact">
        <Question id="name" type="text" required label="Full Name" />
        <Row>
          <Question id="email" type="email" required label="Email" />
          <Question id="phone" type="tel" label="Phone" format="phone" />
        </Row>
      </Section>
    </Form>
  )
}

Every form follows the pattern: Form > Section > Question. Layout components (Row, Card, Banner, etc.) go inside sections. The id on each Question must be unique within the form.


Core Components

<Form>

Root container. Handles collection, rendering, state, navigation, and submission.

PropTypeDefaultDescription
idstringrequiredUnique form identifier (used for deploy)
versionstringundefinedForm version string
titlestringForm title (shown in header if using FormTitle)
onSubmit(answers, inputLog, remarks?) => voidSubmission handler
onSave(answers, inputLog, remarks?) => voidManual save handler
initialAnswersRecord<string, unknown>Pre-populate answers
initialInputLogInputLogEntry[]Resume from existing input log
navigation'none' | 'sidebar' | 'stepper' | 'tabs''none'Multi-section navigation mode
theme'default' | 'printed''default'Visual theme
compactbooleanfalseReduced spacing
width'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Form width preset (480/640/800/1000/1200px)
submitLabelstring"Submit"Submit button text
nextLabelstring"Continue"Next button text
backLabelstring"Back"Back button text
ctaLabelstringCTA button text (e.g., "Save For Later")
hideActionsbooleanfalseHide navigation buttons
autoSavebooleanfalseEnable auto-save
autoSaveDelaynumber1500Auto-save debounce (ms)
saveIndicator'checkmark' | 'toast' | 'none''toast'Auto-save feedback style
onAutoSave(answers, inputLog, remarks?) => void | Promise<void>Auto-save handler
enableRoutingbooleanfalseURL-based section routing
basePathstringBase path for URL routing
blockNavigationbooleanfalsePrevent section navigation until current is valid
readOnlybooleanfalseRead-only mode
previewbooleanfalsePreview mode
debugbooleanfalseShow debug panel
classstringCSS class on form root
seriesstringSeries slug for prefill grouping
prefill'all' | 'partial'Prefill mode
apiKeystringPublishable key for connected mode (fpk_*)
serverUrlstringwindow.location.originServer URL for connected mode
identityTokenstringJWT for identity verification
onSubmitSuccess(result) => voidCalled after successful connected-mode submit
onFormStateReady(formState) => voidCalled when form state is initialized
onManifestReady(manifest) => voidCalled when manifest is built
detectStaleSessionbooleanfalseEnable stale tab detection
staleThresholdnumber60000Time in ms before session is considered stale
showSaveStatusbooleanfalseShow persistent "Last saved X ago" status
environmentEnvironmentInfoEnvironment info for non-production badge
slotsRecord<string, (ctx) => JSX.Element>Named slot content
componentsRecord<string, (props) => JSX.Element>Custom component overrides

Connected mode: When apiKey is provided, the form automatically creates drafts, auto-saves, and submits to the FormDSL server. No onSubmit needed.


<Section>

Groups questions into navigable steps.

PropTypeDefaultDescription
titlestringrequiredDisplay title (shown in navigation)
idstringderived from titleUnique section ID
slugstringsame as idURL-friendly slug for deep linking
iconJSX.ElementIcon in navigation/header
showIfConditionConditional visibility (section skipped in nav when hidden)
variant'default' | 'plain' | 'compact' | 'highlighted''default'Visual style
fieldLayout'stacked' | 'horizontal''stacked'Label placement (horizontal = side-by-side)
tsx
<Section title="Personal Information" variant="highlighted" fieldLayout="horizontal">
  <Question id="name" type="text" required label="Full Name" />
</Section>

<Question>

Registers a form field. The type prop determines the field type and available props.

Base Props (all field types)

PropTypeDefaultDescription
idstringrequiredUnique field identifier
typeFieldTyperequiredField type (see below)
labelstringDisplay label (supports markdown + {{tokens}})
descriptionstringHelp text below the field (supports markdown + {{tokens}})
visualLabelJSX.ElementJSX override for the visible label (string label still used for manifest/review/a11y)
visualDescriptionJSX.ElementJSX override for the visible description
requiredbooleanfalseField is required
showIfConditionConditional visibility
requiredIfConditionConditionally required (overrides required)
disabledIfConditionConditionally disabled
excludeFromCompletionbooleanfalseSkip in completion percentage
commentablebooleanfalseAllow user remarks/comments on this field
prefillbooleanfalseMark as prefillable from prior submissions
validationMessagestringCustom validation error message (overrides defaults)
componentComponentOverride the default field renderer for any field type

Field Types

text / email / tel / phone / textarea
PropTypeDefaultDescription
placeholderstringPlaceholder text
minLengthnumberMinimum character length
maxLengthnumberMaximum character length
patternstringRegex validation pattern
patternHintstringHint shown when pattern validation fails
formatTextFieldFormatPreset input mask (see Masking section)
maskstringCustom IMask pattern
maskedbooleanObscure value on blur (auto-true for ssn, itin)
maskedVisibleCharsnumberChars visible from the end when masked
maskPlaceholderstring'_'Placeholder char in mask
rowsnumberTextarea rows
matchstringID of another field whose value must match (e.g., confirm email)
tsx
<Question id="ssn" type="text" required label="SSN" format="ssn" />
<Question id="notes" type="textarea" label="Notes" rows={4} maxLength={500} />
<Question id="email" type="email" required label="Email" />
<Question id="confirmEmail" type="email" required label="Confirm Email" match="email" />
number
PropTypeDefaultDescription
minnumberMinimum value
maxnumberMaximum value
stepnumberStep increment
placeholderstringPlaceholder text
prefixstringText before number (e.g., "$")
suffixstringText after number (e.g., "sq ft")
thousandsSeparatorstring','Thousands separator
decimalSeparatorstring'.'Decimal separator
scalenumber0Decimal places
padFractionalZerosbooleanfalsePad with trailing zeros
formulastringComputed formula (makes field read-only)
tsx
<Question id="qty" type="number" required label="Quantity" min={1} max={100} />
<Question id="total" type="number" label="Total" formula="{{qty}} * {{price}}" prefix="$" scale={2} />
currency
PropTypeDefaultDescription
currencystring'USD'Currency code
symbolstringauto from codeCustom symbol
localestring'en-US'Locale
minnumberMinimum value
maxnumberMaximum value
symbolPosition'prefix' | 'suffix''prefix'Symbol placement
thousandsSeparatorstring','Thousands separator
decimalSeparatorstring'.'Decimal separator
scalenumber2Decimal places
showCentsbooleantruePad fractional zeros
placeholderstringPlaceholder text
formulastringComputed formula (makes field read-only)
tsx
<Question id="salary" type="currency" required label="Annual Salary" />
<Question id="total" type="currency" label="Total" formula="{{subtotal}} + {{tax}}" />
date / datetime
PropTypeDefaultDescription
minstringMinimum date (ISO string)
maxstringMaximum date (ISO string)
formatstringDisplay format
timezonestringTimezone (datetime only)
tsx
<Question id="dob" type="date" required label="Date of Birth" max="2008-01-01" />
time
PropTypeDefaultDescription
minstringMinimum time (HH:MM)
maxstringMaximum time (HH:MM)
tsx
<Question id="appointmentTime" type="time" required label="Preferred Time" min="09:00" max="17:00" />
year
PropTypeDefaultDescription
minnumberMinimum year
maxnumberMaximum year
placeholderstringPlaceholder
month

No additional props.

tsx
<Question id="birthMonth" type="month" label="Birth Month" />
select / radio
PropTypeDefaultDescription
options{ value: string, label: string, exclusive?: boolean }[]requiredChoices
multiplebooleanfalseAllow multiple selections (select only)
searchablebooleanfalseAutocomplete input (select only)
tsx
<Question id="color" type="select" required label="Favorite Color" options={[
  { value: 'red', label: 'Red' },
  { value: 'blue', label: 'Blue' },
  { value: 'green', label: 'Green' },
]} />

<Question id="size" type="radio" required label="Size" options={[
  { value: 's', label: 'Small' },
  { value: 'm', label: 'Medium' },
  { value: 'l', label: 'Large' },
]} />
multicheck
PropTypeDefaultDescription
options{ value: string, label: string, exclusive?: boolean }[]requiredChoices
minSelectnumberMinimum selections
maxSelectnumberMaximum selections

The exclusive option clears all other selections when chosen (e.g., "None of the above").

tsx
<Question id="interests" type="multicheck" label="Interests" options={[
  { value: 'sports', label: 'Sports' },
  { value: 'music', label: 'Music' },
  { value: 'none', label: 'None of the above', exclusive: true },
]} />
boolean / checkbox

No additional props. Renders a toggle/checkbox. Value is true/false.

tsx
<Question id="agree" type="checkbox" required label="I agree to the terms" />
yesno
PropTypeDefaultDescription
yesLabelstring"Yes"Custom yes label
noLabelstring"No"Custom no label
tsx
<Question id="hasDependents" type="yesno" label="Do you have dependents?" />
rating
PropTypeDefaultDescription
maxnumber5Maximum rating
variant'stars' | 'numbers''stars'Display variant
tsx
<Question id="satisfaction" type="rating" label="How satisfied are you?" max={10} variant="numbers" />
file
PropTypeDefaultDescription
acceptstringAccepted MIME types (e.g., "image/*,.pdf")
maxSizenumberMax file size in bytes
multiplebooleanfalseAllow multiple files
tsx
<Question id="resume" type="file" required label="Upload Resume" accept=".pdf,.doc,.docx" />
signature

No additional props. Renders a signature pad.

tsx
<Question id="signature" type="signature" required label="Your Signature" />
tags
PropTypeDefaultDescription
options{ value: string, label: string }[]Predefined tag options
maxTagsnumberMaximum number of tags
allowCustombooleantrueAllow free-text tags
placeholderstringPlaceholder
tsx
<Question id="skills" type="tags" label="Skills" maxTags={5} options={[
  { value: 'js', label: 'JavaScript' },
  { value: 'ts', label: 'TypeScript' },
]} />
matrix
PropTypeDefaultDescription
rows{ id: string, label: string }[]requiredRow definitions
columns{ value: string, label: string }[]requiredColumn options

Table-style radio grid, commonly used for Likert scales. Stores { [rowId]: selectedColumnValue }.

tsx
<Question id="satisfaction" type="matrix" required label="Rate each area"
  rows={[
    { id: 'quality', label: 'Product Quality' },
    { id: 'support', label: 'Customer Support' },
    { id: 'value', label: 'Value for Money' },
  ]}
  columns={[
    { value: '1', label: 'Poor' },
    { value: '2', label: 'Fair' },
    { value: '3', label: 'Good' },
    { value: '4', label: 'Excellent' },
  ]}
/>
url
PropTypeDefaultDescription
placeholderstringPlaceholder text
tsx
<Question id="website" type="url" label="Website" placeholder="https://example.com" />
geo
PropTypeDefaultDescription
defaultZoomnumberInitial map zoom level

Stores { lat: number, lng: number }.

address

No additional props. Stores a structured address object.

country
PropTypeDefaultDescription
placeholderstringPlaceholder

Searchable country selector. Internally maps to text.

tsx
<Question id="country" type="country" required label="Country" />
ranking
PropTypeDefaultDescription
options{ value: string, label: string }[]requiredItems to rank

Drag-and-drop reorderable list. Value is an ordered array.

tsx
<Question id="priorities" type="ranking" label="Rank your priorities" options={[
  { value: 'cost', label: 'Cost' },
  { value: 'quality', label: 'Quality' },
  { value: 'speed', label: 'Speed' },
]} />
hidden
PropTypeDefaultDescription
valueunknownrequiredStatic value to store

Not rendered. Use for tracking data.

tsx
<Question id="source" type="hidden" value="landing-page-v2" />
custom
PropTypeDefaultDescription
componentComponentrequiredCustom field component

For custom field rendering. Not available on hosted pages.


<Repeat>

Repeatable group of fields. Users can add/remove items.

PropTypeDefaultDescription
idstringrequiredUnique repeat group ID
labelstringGroup label
minItemsnumberMinimum number of items
maxItemsnumberMaximum number of items
showIfConditionConditional visibility
excludeFromCompletionbooleanfalseSkip in completion %
variant'card' | 'table''card'Display variant
itemLabelstringLabel template per item (supports {{fieldId}}, {{index}}, {{index1}})
addButtonLabelstring | { zero: string, oneOrMore: string }Custom add button text
collapsiblebooleanfalseEdit/Done toggle (card variant)
startCollapsedbooleanfalseNew items start collapsed
forceEditbooleanfalseAlways in edit mode
autoAddFirstbooleanfalseAuto-add first item when empty

Children define the template for one item.

tsx
<Repeat id="dependents" label="Dependents" itemLabel="{{firstName}} {{lastName}}"
  collapsible autoAddFirst minItems={0} maxItems={10}>
  <Row>
    <Question id="firstName" type="text" required label="First Name" />
    <Question id="lastName" type="text" required label="Last Name" />
  </Row>
  <Question id="relationship" type="select" required label="Relationship" options={[
    { value: 'child', label: 'Child' },
    { value: 'spouse', label: 'Spouse' },
    { value: 'parent', label: 'Parent' },
  ]} />
  <Question id="dob" type="date" required label="Date of Birth" />
</Repeat>

Nested repeats are supported:

tsx
<Repeat id="projects" label="Projects" itemLabel="Project {{index1}}">
  <Question id="projectName" type="text" required label="Project Name" />
  <Repeat id="tasks" label="Tasks" itemLabel="Task {{index1}}" variant="table">
    <Question id="taskName" type="text" required label="Task" />
    <Question id="hours" type="number" label="Hours" min={0} />
  </Repeat>
</Repeat>

<Review>

Summary/review section. Always appears last in navigation.

PropTypeDefaultDescription
titlestring"Review"Section title
idstring"review"Section ID
slugstring"review"URL slug
iconJSX.ElementNavigation icon
showIfConditionConditional visibility

Auto mode (no children): generates a summary of all sections and fields automatically.

Custom mode (with children): you control the layout using <Answer> components.

tsx
{/* Auto mode */}
<Review />

{/* Custom mode */}
<Review title="Summary">
  <Card title="Contact Info">
    <Row>
      <Answer id="name" />
      <Answer id="email" />
    </Row>
  </Card>
</Review>

<Answer>

Read-only display of a field value. Used inside <Review> for custom layouts.

PropTypeDefaultDescription
idstringrequiredField ID to display (must match a Question)
labelstringOverride label
hideLabelbooleanfalseHide the label
showIfConditionConditional visibility

Layout Components

<Row>

Horizontal layout for side-by-side fields.

PropTypeDefaultDescription
colsnumberchildren countNumber of equal columns
gap'sm' | 'md' | 'lg''md'Gap between columns
align'start' | 'center' | 'end' | 'stretch''stretch'Vertical alignment
compactbooleanfalseNatural column widths, not stretched
showIfConditionConditional visibility
tsx
<Row>
  <Question id="firstName" type="text" required label="First Name" />
  <Question id="lastName" type="text" required label="Last Name" />
</Row>

<Stack>

Vertical layout with controlled spacing.

PropTypeDefaultDescription
gap'sm' | 'md' | 'lg''md'Gap between items
showIfConditionConditional visibility

<Card>

Visual container with optional title.

PropTypeDefaultDescription
titlestringCard title
showIfConditionConditional visibility
tsx
<Card title="Spouse Information">
  <Row>
    <Question id="spouseName" type="text" required label="Name" />
    <Question id="spouseSsn" type="text" required label="SSN" format="ssn" />
  </Row>
</Card>

<Content>

Conditional content block. Supports static children or dynamic render function.

PropTypeDefaultDescription
showIfConditionConditional visibility
render(ctx: FormStateContext) => JSX.ElementDynamic render function
tsx
{/* Static content, conditionally shown */}
<Content showIf={{ filingStatus: 'married' }}>
  <Card title="Spouse Details">
    <Question id="spouseName" type="text" required label="Spouse Name" />
  </Card>
</Content>

{/* Dynamic content using form state */}
<Content showIf={{ qty: { gt: 0 } }} render={(ctx) => {
  const qty = (ctx.getAnswer('qty') as number) || 0
  const price = (ctx.getAnswer('price') as number) || 0
  return <p>Subtotal: ${(qty * price).toFixed(2)}</p>
}} />

Info/warning/error/success message box.

PropTypeDefaultDescription
variant'info' | 'warning' | 'error' | 'success'requiredVisual style
titlestringBanner title
showIfConditionConditional visibility

Children must be a string (supports markdown and {{tokens}}).

tsx
<Banner variant="warning" title="Important">
  Filing jointly may provide more tax benefits for married couples.
</Banner>

<Banner variant="info" showIf={{ country: 'US' }}>
  US residents must report worldwide income.
</Banner>

<Divider>

Visual horizontal separator.

PropTypeDefaultDescription
showIfConditionConditional visibility

These components control the form chrome. Place them at the top level inside <Form>, outside sections.

Container for top/bottom content. Wrap atom components inside.

PropTypeDefaultDescription
showIfConditionConditional visibility

<SidebarNav> / <StepperNav>

Section navigation widgets.

PropTypeDefaultDescription
showLabelsbooleantrueShow section labels
showIfConditionConditional visibility

<FormContent>

Placeholder for the active section content area. Used when building custom layouts.

<FormTitle>

Renders the form title.

PropTypeDefaultDescription
textstringOverride title (defaults to Form's title prop)

<ProgressBar>

Form completion progress indicator.

PropTypeDefaultDescription
labelbooleanShow percentage label
scope'form' | 'section''form'Progress scope
sectionIdstringSpecific section to track

<AutoSaveStatus>

Auto-save status indicator (shows "Saving...", "Saved", etc.).

Composite back/next/submit button bar.

PropTypeDefaultDescription
submitLabelstring"Submit"Submit button text
saveLabelstringSave button text (only shown if provided)
backLabelstring"Back"Back button text
nextLabelstring"Continue"Next button text

Individual Buttons

<SaveButton>, <SubmitButton>, <BackButton>, <NextButton> - each has label?: string and showIf?: Condition.

<NextButton> also accepts submitLabel?: string for the text shown when on the last section.

<Slot>

Injection point for host-provided content.

PropTypeDefaultDescription
namestringrequiredSlot identifier
showIfConditionConditional visibility

Full Custom Layout Example

tsx
<Form id="wizard" version="1.0" navigation="sidebar" title="Application" autoSave>
  <Header>
    <FormTitle />
    <AutoSaveStatus />
    <SaveButton label="Save Draft" />
    <SubmitButton />
  </Header>
  <SidebarNav />
  <FormContent />

  <Section title="Step 1">
    <Question id="name" type="text" required label="Name" />
  </Section>
  <Section title="Step 2">
    <Question id="email" type="email" required label="Email" />
  </Section>
  <Review />
</Form>

Conditions

Conditions control visibility (showIf), requirement (requiredIf), and disabled state (disabledIf). A condition is an object where keys are field IDs and values define the match.

Syntax

tsx
// Simple equality: field equals value
showIf={{ country: "US" }}

// Multiple values (OR): field equals any value in the array
showIf={{ country: ["US", "CA", "MX"] }}

// Operators: numeric/comparison operators
showIf={{ age: { gte: 18 } }}
showIf={{ income: { gt: 0, lte: 1000000 } }}  // can combine operators

// Multiple fields (AND): all conditions must match
showIf={{ country: "US", age: { gte: 18 } }}

// Boolean fields
showIf={{ hasDependents: true }}

// Not equals
showIf={{ status: { ne: "declined" } }}

// Logical OR
showIf={{ $or: [{ country: "US" }, { days: { gte: 183 } }] }}

// Logical NOT
showIf={{ $not: { country: "US" } }}

// Array membership (for multicheck/tags)
showIf={{ interests: { in: ["sports"] } }}     // value contains any of these
showIf={{ interests: { nin: ["excluded"] } }}   // value does not contain any of these

Operators

OperatorDescriptionExample
eqEquals{ field: { eq: "value" } }
neNot equals{ field: { ne: "" } }
gtGreater than{ age: { gt: 17 } }
gteGreater than or equal{ age: { gte: 18 } }
ltLess than{ score: { lt: 100 } }
lteLess than or equal{ score: { lte: 100 } }
inValue in array{ role: { in: ["admin", "editor"] } }
ninValue not in array{ role: { nin: ["guest"] } }

Logical Operators

OperatorDescriptionExample
$orAny condition matches{ $or: [{ country: "US" }, { country: "CA" }] }
$notCondition does not match{ $not: { status: "declined" } }

Dot Notation

For nested fields (inside repeats), use dot notation:

tsx
showIf={{ "spouse.employed": true }}
showIf={{ "dependents.0.age": { lt: 18 } }}

Usage on Components

tsx
{/* Conditional question */}
<Question id="spouseName" type="text" label="Spouse Name" showIf={{ married: true }} />

{/* Conditionally required */}
<Question id="phone" type="tel" label="Phone" requiredIf={{ contactMethod: "phone" }} />

{/* Conditionally disabled */}
<Question id="total" type="number" label="Total" disabledIf={{ autoCalc: true }} />

{/* Conditional section (skipped in navigation) */}
<Section title="Spouse Info" showIf={{ filingStatus: "married" }}>
  ...
</Section>

{/* Conditional layout */}
<Row showIf={{ showDetails: true }}>
  <Question id="detail1" type="text" label="Detail 1" />
  <Question id="detail2" type="text" label="Detail 2" />
</Row>

<Banner variant="warning" showIf={{ income: { gt: 200000 } }}>
  You may be subject to AMT.
</Banner>

Formulas

Computed fields that auto-calculate based on other field values. The formula prop is available on number and currency fields. Fields with formulas become read-only.

Syntax

Use {{fieldId}} to reference other fields. Supports +, -, *, /, % operators, parentheses, and functions.

tsx
<Question id="qty" type="number" label="Quantity" />
<Question id="price" type="currency" label="Unit Price" />
<Question id="subtotal" type="currency" label="Subtotal" formula="{{qty}} * {{price}}" />
<Question id="tax" type="currency" label="Tax" formula="{{subtotal}} * 0.08" />
<Question id="total" type="currency" label="Total" formula="{{subtotal}} + {{tax}}" />

{/* With parentheses */}
<Question id="discounted" type="currency" label="After Discount"
  formula="{{subtotal}} * (1 - {{discountRate}} / 100)" />

Built-in Functions

FunctionDescriptionExample
SUMSum of valuesSUM({{a}}, {{b}}, {{c}})
COUNTCount of non-empty valuesCOUNT({{a}}, {{b}}, {{c}})
MINMinimum valueMIN({{a}}, {{b}})
MAXMaximum valueMAX({{a}}, {{b}})
IFConditional valueIF({{qty}} > 0, {{qty}} * {{price}}, 0)
ROUNDRound to N decimalsROUND({{total}}, 2)

Formulas are compiled to RPN (Reverse Polish Notation) automatically. If any referenced field is empty/undefined, the formula returns undefined (blank).


Token Interpolation

{{fieldId}} tokens can be used in label, description, itemLabel, and Banner content. They are replaced with the current value of the referenced field.

Special Tokens

TokenDescriptionExample
{{fieldId}}Current value of a field"Hello, {{firstName}}"
{{index}}0-based repeat item index"Item {{index}}"
{{index1}}1-based repeat item index"Employee #{{index1}}"

Resolution Order

  1. Special tokens ({{index}}, {{index1}})
  2. Current repeat item scope values
  3. Root form values
  4. Dot notation for nested paths ({{spouse.name}})
tsx
<Question id="greeting" type="text" label="Greeting for {{firstName}}" />

<Repeat id="employees" itemLabel="Employee #{{index1}}: {{name}}">
  <Question id="name" type="text" required label="Name" />
</Repeat>

<Banner variant="info">
  Thank you, **{{firstName}}**! Your application for {{position}} has been received.
</Banner>

Masking

Input masks enforce formatting for text fields. Use the format prop for presets or mask for custom patterns.

Format Presets

FormatPatternExample
ssn###-##-####123-45-6789
ein##-#######12-3456789
phone(###) ###-####(555) 123-4567
zip#####90210
zip4#####-####90210-1234
routingNumber#########021000021
itin###-##-####900-70-0000
canadianPostalA#A #A#K1A 0B1
mmyy##/##03/26
mmddyyyy##/##/####03/15/2026
isoDate####-##-##2026-03-15
time24##:##14:30
measurement### x ### x ###120 x 60 x 48

ssn and itin are auto-masked on blur (value obscured as ***-**-6789).

tsx
<Question id="ssn" type="text" required label="Social Security Number" format="ssn" />
<Question id="phone" type="tel" required label="Phone" format="phone" />
<Question id="zip" type="text" required label="ZIP Code" format="zip" />

Custom Masks

Use mask for custom patterns. 0 = required digit, a = required letter, # = maskable digit.

tsx
<Question id="custom" type="text" label="Custom ID" mask="AA-0000-00" />

Masked Visibility

tsx
{/* Auto-masked for SSN format */}
<Question id="ssn" type="text" format="ssn" label="SSN" />

{/* Explicitly mask any field */}
<Question id="accountNo" type="text" label="Account" masked maskedVisibleChars={4} />

Markdown in Labels

Labels and descriptions support inline markdown: **bold**, *italic*, `code`, [link](url).

tsx
<Question id="agree" type="checkbox" required
  label="I agree to the **[Terms of Service](https://example.com/tos)**" />

Complete Examples

A. Simple Contact Form

tsx
import { Form, Section, Question, Row } from '@formdsl/solid'

export default function ContactForm() {
  return (
    <Form id="contact" version="1.0" onSubmit={(answers) => console.log(answers)}>
      <Section title="Contact Information">
        <Row>
          <Question id="firstName" type="text" required label="First Name" />
          <Question id="lastName" type="text" required label="Last Name" />
        </Row>
        <Question id="email" type="email" required label="Email Address" />
        <Question id="phone" type="tel" label="Phone Number" format="phone" />
        <Question id="department" type="select" label="Department" options={[
          { value: 'sales', label: 'Sales' },
          { value: 'support', label: 'Support' },
          { value: 'billing', label: 'Billing' },
        ]} />
        <Question id="message" type="textarea" required label="Message" rows={4} />
        <Question id="newsletter" type="checkbox" label="Subscribe to newsletter" />
      </Section>
    </Form>
  )
}

B. Job Application

tsx
import { Form, Section, Question, Row, Repeat, Card, Banner, Review } from '@formdsl/solid'

export default function JobApplication() {
  return (
    <Form id="job-application" version="1.0" navigation="stepper" title="Job Application">
      <Section title="Personal Info">
        <Row>
          <Question id="firstName" type="text" required label="First Name" />
          <Question id="lastName" type="text" required label="Last Name" />
        </Row>
        <Row>
          <Question id="email" type="email" required label="Email" />
          <Question id="phone" type="tel" required label="Phone" format="phone" />
        </Row>
        <Question id="position" type="select" required label="Position Applied For" options={[
          { value: 'engineer', label: 'Software Engineer' },
          { value: 'designer', label: 'Product Designer' },
          { value: 'pm', label: 'Product Manager' },
          { value: 'other', label: 'Other' },
        ]} />
        <Question id="otherPosition" type="text" required label="Specify Position"
          showIf={{ position: 'other' }} />
        <Question id="salary" type="currency" label="Expected Salary" />
        <Question id="website" type="url" label="Portfolio / Website" />
      </Section>

      <Section title="Experience">
        <Repeat id="workHistory" label="Work History" itemLabel="{{company}} - {{title}}"
          collapsible autoAddFirst maxItems={10}
          addButtonLabel={{ zero: 'Add your first job', oneOrMore: 'Add another job' }}>
          <Row>
            <Question id="company" type="text" required label="Company" />
            <Question id="title" type="text" required label="Job Title" />
          </Row>
          <Row>
            <Question id="startDate" type="date" required label="Start Date" />
            <Question id="endDate" type="date" label="End Date" />
          </Row>
          <Question id="current" type="checkbox" label="I currently work here" />
          <Question id="description" type="textarea" label="Responsibilities" rows={3}
            excludeFromCompletion />
        </Repeat>
        <Question id="resume" type="file" required label="Upload Resume"
          accept=".pdf,.doc,.docx" />
      </Section>

      <Review />
    </Form>
  )
}

C. Tax Questionnaire (excerpt)

tsx
import {
  Form, Section, Question, Row, Repeat, Card, Content, Banner,
  Header, Footer, FormTitle, FormContent, SidebarNav,
  SaveButton, SubmitButton, AutoSaveStatus, Divider, Review,
} from '@formdsl/solid'

export default function TaxForm(props) {
  return (
    <Form
      id="tax-2025" version="1.0"
      navigation="sidebar" title="Tax Questionnaire 2025"
      autoSave saveIndicator="checkmark" compact
      series="annual-tax" prefill="all"
      onSubmit={props.onSubmit} onAutoSave={props.onAutoSave}
      initialAnswers={props.initialAnswers}
      initialInputLog={props.initialInputLog}
    >
      <Header>
        <FormTitle />
        <AutoSaveStatus />
        <SaveButton label="Save Draft" />
        <SubmitButton />
      </Header>
      <SidebarNav />
      <FormContent />

      {/* Section 1: Filing Status */}
      <Section title="Filing Status" variant="highlighted">
        <Question id="filingStatus" type="radio" required label="Filing Status" options={[
          { value: 'single', label: 'Single' },
          { value: 'married', label: 'Married Filing Jointly' },
          { value: 'separate', label: 'Married Filing Separately' },
          { value: 'head', label: 'Head of Household' },
        ]} />
        <Banner variant="info" title="Filing Status Tip" showIf={{ filingStatus: 'married' }}>
          Filing jointly generally provides more tax benefits for married couples.
        </Banner>
      </Section>

      {/* Section 2: Personal Information */}
      <Section title="Personal Information">
        <Card title="You">
          <Row>
            <Question id="firstName" type="text" required label="First Name" prefill />
            <Question id="lastName" type="text" required label="Last Name" prefill />
          </Row>
          <Question id="ssn" type="text" required label="SSN" format="ssn" />
          <Row>
            <Question id="dob" type="date" required label="Date of Birth" />
            <Question id="phone" type="tel" label="Phone" format="phone" />
          </Row>
        </Card>

        <Content showIf={{ filingStatus: ['married', 'separate'] }}>
          <Card title="Spouse">
            <Row>
              <Question id="spouseFirst" type="text" required label="First Name" />
              <Question id="spouseLast" type="text" required label="Last Name" />
            </Row>
            <Question id="spouseSsn" type="text" required label="Spouse SSN" format="ssn" />
          </Card>
        </Content>
      </Section>

      {/* Section 3: Dependents */}
      <Section title="Dependents">
        <Question id="hasDependents" type="yesno" label="Do you have dependents?" />
        <Repeat id="dependents" showIf={{ hasDependents: true }}
          itemLabel="{{depFirstName}} {{depLastName}}" collapsible autoAddFirst>
          <Row>
            <Question id="depFirstName" type="text" required label="First Name" />
            <Question id="depLastName" type="text" required label="Last Name" />
          </Row>
          <Row>
            <Question id="depSsn" type="text" required label="SSN" format="ssn" />
            <Question id="depRelation" type="select" required label="Relationship" options={[
              { value: 'child', label: 'Child' },
              { value: 'parent', label: 'Parent' },
              { value: 'other', label: 'Other' },
            ]} />
          </Row>
          <Question id="depDob" type="date" required label="Date of Birth" />
          <Question id="depMonths" type="number" required label="Months lived with you"
            min={0} max={12} />
        </Repeat>
      </Section>

      {/* Section 4: Income */}
      <Section title="Income">
        <Question id="wages" type="currency" required label="Wages & Salary (W-2)" />
        <Question id="interest" type="currency" label="Interest Income" />
        <Question id="dividends" type="currency" label="Dividend Income" />
        <Question id="selfEmployed" type="yesno" label="Self-employment income?" />
        <Content showIf={{ selfEmployed: true }}>
          <Card title="Self-Employment">
            <Question id="businessIncome" type="currency" required label="Gross Income" />
            <Question id="businessExpenses" type="currency" required label="Total Expenses" />
            <Question id="netIncome" type="currency" label="Net Income"
              formula="{{businessIncome}} - {{businessExpenses}}" />
          </Card>
        </Content>

        <Divider />

        <Question id="totalIncome" type="currency" label="Total Income"
          formula="{{wages}} + {{interest}} + {{dividends}} + {{netIncome}}" />
      </Section>

      {/* Section 5: Deductions */}
      <Section title="Deductions">
        <Question id="deductionType" type="radio" required label="Deduction Method" options={[
          { value: 'standard', label: 'Standard Deduction' },
          { value: 'itemized', label: 'Itemize Deductions' },
        ]} />

        <Content showIf={{ deductionType: 'itemized' }}>
          <Question id="mortgage" type="currency" label="Mortgage Interest" />
          <Question id="stateTax" type="currency" label="State/Local Taxes (max $10,000)" max={10000} />
          <Question id="charity" type="currency" label="Charitable Contributions" />
          <Question id="medical" type="currency" label="Medical Expenses" />
          <Question id="itemizedTotal" type="currency" label="Total Itemized Deductions"
            formula="{{mortgage}} + {{stateTax}} + {{charity}} + {{medical}}" />
        </Content>

        <Banner variant="warning" showIf={{ deductionType: 'itemized', itemizedTotal: { lt: 14600 } }}>
          Your itemized deductions are less than the standard deduction ($14,600).
          Consider using the standard deduction instead.
        </Banner>
      </Section>

      <Review title="Review & Submit" />
    </Form>
  )
}

Tips & Anti-patterns

  1. Every Question needs a unique id within the form. IDs are used for data binding, conditions, and formulas.

  2. Use Row for side-by-side layout, not CSS. The renderer handles responsive behavior.

  3. Sections with showIf are automatically skipped in navigation when hidden.

  4. Options must have value and label:

    tsx
    // Correct
    options={[{ value: 'us', label: 'United States' }]}
    // Wrong
    options={['us', 'ca']}
  5. Don't combine required and requiredIf on the same field. requiredIf takes precedence when present.

  6. Repeat children define the template for one item. Don't wrap them in their own Section.

  7. Formula fields are automatically read-only. Don't add disabled or similar props.

  8. Content with render receives FormStateContext which has:

    • getAnswer(fieldId) - get a field value
    • setAnswer(fieldId, value) - set a field value
    • answers - all current answers
  9. Import patterns - use the package name, not relative paths:

    tsx
    // SolidJS
    import { Form, Section, Question } from '@formdsl/solid'
    // React
    import { Form, Section, Question } from '@formdsl/react'
  10. Connected mode (embed with API key) handles everything automatically - no onSubmit needed:

    tsx
    <Form id="my-form" version="1.0" apiKey="fdk_live_xxxxx">
      ...
    </Form>
  11. Field matching for confirmation fields:

    tsx
    <Question id="email" type="email" required label="Email" />
    <Question id="confirmEmail" type="email" required label="Confirm Email" match="email" />
  12. Custom validation messages override defaults:

    tsx
    <Question id="age" type="number" required min={18}
      validationMessage="You must be at least 18 years old" />