Appearance
FormDSL Reference
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.
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique form identifier (used for deploy) |
version | string | undefined | Form version string |
title | string | Form title (shown in header if using FormTitle) | |
onSubmit | (answers, inputLog, remarks?) => void | Submission handler | |
onSave | (answers, inputLog, remarks?) => void | Manual save handler | |
initialAnswers | Record<string, unknown> | Pre-populate answers | |
initialInputLog | InputLogEntry[] | Resume from existing input log | |
navigation | 'none' | 'sidebar' | 'stepper' | 'tabs' | 'none' | Multi-section navigation mode |
theme | 'default' | 'printed' | 'default' | Visual theme |
compact | boolean | false | Reduced spacing |
width | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Form width preset (480/640/800/1000/1200px) |
submitLabel | string | "Submit" | Submit button text |
nextLabel | string | "Continue" | Next button text |
backLabel | string | "Back" | Back button text |
ctaLabel | string | CTA button text (e.g., "Save For Later") | |
hideActions | boolean | false | Hide navigation buttons |
autoSave | boolean | false | Enable auto-save |
autoSaveDelay | number | 1500 | Auto-save debounce (ms) |
saveIndicator | 'checkmark' | 'toast' | 'none' | 'toast' | Auto-save feedback style |
onAutoSave | (answers, inputLog, remarks?) => void | Promise<void> | Auto-save handler | |
enableRouting | boolean | false | URL-based section routing |
basePath | string | Base path for URL routing | |
blockNavigation | boolean | false | Prevent section navigation until current is valid |
readOnly | boolean | false | Read-only mode |
preview | boolean | false | Preview mode |
debug | boolean | false | Show debug panel |
class | string | CSS class on form root | |
series | string | Series slug for prefill grouping | |
prefill | 'all' | 'partial' | Prefill mode | |
apiKey | string | Publishable key for connected mode (fpk_*) | |
serverUrl | string | window.location.origin | Server URL for connected mode |
identityToken | string | JWT for identity verification | |
onSubmitSuccess | (result) => void | Called after successful connected-mode submit | |
onFormStateReady | (formState) => void | Called when form state is initialized | |
onManifestReady | (manifest) => void | Called when manifest is built | |
detectStaleSession | boolean | false | Enable stale tab detection |
staleThreshold | number | 60000 | Time in ms before session is considered stale |
showSaveStatus | boolean | false | Show persistent "Last saved X ago" status |
environment | EnvironmentInfo | Environment info for non-production badge | |
slots | Record<string, (ctx) => JSX.Element> | Named slot content | |
components | Record<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.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | Display title (shown in navigation) |
id | string | derived from title | Unique section ID |
slug | string | same as id | URL-friendly slug for deep linking |
icon | JSX.Element | Icon in navigation/header | |
showIf | Condition | Conditional 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)
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique field identifier |
type | FieldType | required | Field type (see below) |
label | string | Display label (supports markdown + {{tokens}}) | |
description | string | Help text below the field (supports markdown + {{tokens}}) | |
visualLabel | JSX.Element | JSX override for the visible label (string label still used for manifest/review/a11y) | |
visualDescription | JSX.Element | JSX override for the visible description | |
required | boolean | false | Field is required |
showIf | Condition | Conditional visibility | |
requiredIf | Condition | Conditionally required (overrides required) | |
disabledIf | Condition | Conditionally disabled | |
excludeFromCompletion | boolean | false | Skip in completion percentage |
commentable | boolean | false | Allow user remarks/comments on this field |
prefill | boolean | false | Mark as prefillable from prior submissions |
validationMessage | string | Custom validation error message (overrides defaults) | |
component | Component | Override the default field renderer for any field type |
Field Types
text / email / tel / phone / textarea
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | Placeholder text | |
minLength | number | Minimum character length | |
maxLength | number | Maximum character length | |
pattern | string | Regex validation pattern | |
patternHint | string | Hint shown when pattern validation fails | |
format | TextFieldFormat | Preset input mask (see Masking section) | |
mask | string | Custom IMask pattern | |
masked | boolean | Obscure value on blur (auto-true for ssn, itin) | |
maskedVisibleChars | number | Chars visible from the end when masked | |
maskPlaceholder | string | '_' | Placeholder char in mask |
rows | number | Textarea rows | |
match | string | ID 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
| Prop | Type | Default | Description |
|---|---|---|---|
min | number | Minimum value | |
max | number | Maximum value | |
step | number | Step increment | |
placeholder | string | Placeholder text | |
prefix | string | Text before number (e.g., "$") | |
suffix | string | Text after number (e.g., "sq ft") | |
thousandsSeparator | string | ',' | Thousands separator |
decimalSeparator | string | '.' | Decimal separator |
scale | number | 0 | Decimal places |
padFractionalZeros | boolean | false | Pad with trailing zeros |
formula | string | Computed 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
| Prop | Type | Default | Description |
|---|---|---|---|
currency | string | 'USD' | Currency code |
symbol | string | auto from code | Custom symbol |
locale | string | 'en-US' | Locale |
min | number | Minimum value | |
max | number | Maximum value | |
symbolPosition | 'prefix' | 'suffix' | 'prefix' | Symbol placement |
thousandsSeparator | string | ',' | Thousands separator |
decimalSeparator | string | '.' | Decimal separator |
scale | number | 2 | Decimal places |
showCents | boolean | true | Pad fractional zeros |
placeholder | string | Placeholder text | |
formula | string | Computed 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
| Prop | Type | Default | Description |
|---|---|---|---|
min | string | Minimum date (ISO string) | |
max | string | Maximum date (ISO string) | |
format | string | Display format | |
timezone | string | Timezone (datetime only) |
tsx
<Question id="dob" type="date" required label="Date of Birth" max="2008-01-01" />time
| Prop | Type | Default | Description |
|---|---|---|---|
min | string | Minimum time (HH:MM) | |
max | string | Maximum time (HH:MM) |
tsx
<Question id="appointmentTime" type="time" required label="Preferred Time" min="09:00" max="17:00" />year
| Prop | Type | Default | Description |
|---|---|---|---|
min | number | Minimum year | |
max | number | Maximum year | |
placeholder | string | Placeholder |
month
No additional props.
tsx
<Question id="birthMonth" type="month" label="Birth Month" />select / radio
| Prop | Type | Default | Description |
|---|---|---|---|
options | { value: string, label: string, exclusive?: boolean }[] | required | Choices |
multiple | boolean | false | Allow multiple selections (select only) |
searchable | boolean | false | Autocomplete 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
| Prop | Type | Default | Description |
|---|---|---|---|
options | { value: string, label: string, exclusive?: boolean }[] | required | Choices |
minSelect | number | Minimum selections | |
maxSelect | number | Maximum 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
| Prop | Type | Default | Description |
|---|---|---|---|
yesLabel | string | "Yes" | Custom yes label |
noLabel | string | "No" | Custom no label |
tsx
<Question id="hasDependents" type="yesno" label="Do you have dependents?" />rating
| Prop | Type | Default | Description |
|---|---|---|---|
max | number | 5 | Maximum rating |
variant | 'stars' | 'numbers' | 'stars' | Display variant |
tsx
<Question id="satisfaction" type="rating" label="How satisfied are you?" max={10} variant="numbers" />file
| Prop | Type | Default | Description |
|---|---|---|---|
accept | string | Accepted MIME types (e.g., "image/*,.pdf") | |
maxSize | number | Max file size in bytes | |
multiple | boolean | false | Allow 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
| Prop | Type | Default | Description |
|---|---|---|---|
options | { value: string, label: string }[] | Predefined tag options | |
maxTags | number | Maximum number of tags | |
allowCustom | boolean | true | Allow free-text tags |
placeholder | string | Placeholder |
tsx
<Question id="skills" type="tags" label="Skills" maxTags={5} options={[
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
]} />matrix
| Prop | Type | Default | Description |
|---|---|---|---|
rows | { id: string, label: string }[] | required | Row definitions |
columns | { value: string, label: string }[] | required | Column 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
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | Placeholder text |
tsx
<Question id="website" type="url" label="Website" placeholder="https://example.com" />geo
| Prop | Type | Default | Description |
|---|---|---|---|
defaultZoom | number | Initial map zoom level |
Stores { lat: number, lng: number }.
address
No additional props. Stores a structured address object.
country
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | Placeholder |
Searchable country selector. Internally maps to text.
tsx
<Question id="country" type="country" required label="Country" />ranking
| Prop | Type | Default | Description |
|---|---|---|---|
options | { value: string, label: string }[] | required | Items 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
| Prop | Type | Default | Description |
|---|---|---|---|
value | unknown | required | Static value to store |
Not rendered. Use for tracking data.
tsx
<Question id="source" type="hidden" value="landing-page-v2" />custom
| Prop | Type | Default | Description |
|---|---|---|---|
component | Component | required | Custom field component |
For custom field rendering. Not available on hosted pages.
<Repeat>
Repeatable group of fields. Users can add/remove items.
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique repeat group ID |
label | string | Group label | |
minItems | number | Minimum number of items | |
maxItems | number | Maximum number of items | |
showIf | Condition | Conditional visibility | |
excludeFromCompletion | boolean | false | Skip in completion % |
variant | 'card' | 'table' | 'card' | Display variant |
itemLabel | string | Label template per item (supports {{fieldId}}, {{index}}, {{index1}}) | |
addButtonLabel | string | { zero: string, oneOrMore: string } | Custom add button text | |
collapsible | boolean | false | Edit/Done toggle (card variant) |
startCollapsed | boolean | false | New items start collapsed |
forceEdit | boolean | false | Always in edit mode |
autoAddFirst | boolean | false | Auto-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.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | "Review" | Section title |
id | string | "review" | Section ID |
slug | string | "review" | URL slug |
icon | JSX.Element | Navigation icon | |
showIf | Condition | Conditional 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.
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Field ID to display (must match a Question) |
label | string | Override label | |
hideLabel | boolean | false | Hide the label |
showIf | Condition | Conditional visibility |
Layout Components
<Row>
Horizontal layout for side-by-side fields.
| Prop | Type | Default | Description |
|---|---|---|---|
cols | number | children count | Number of equal columns |
gap | 'sm' | 'md' | 'lg' | 'md' | Gap between columns |
align | 'start' | 'center' | 'end' | 'stretch' | 'stretch' | Vertical alignment |
compact | boolean | false | Natural column widths, not stretched |
showIf | Condition | Conditional 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.
| Prop | Type | Default | Description |
|---|---|---|---|
gap | 'sm' | 'md' | 'lg' | 'md' | Gap between items |
showIf | Condition | Conditional visibility |
<Card>
Visual container with optional title.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | Card title | |
showIf | Condition | Conditional 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.
| Prop | Type | Default | Description |
|---|---|---|---|
showIf | Condition | Conditional visibility | |
render | (ctx: FormStateContext) => JSX.Element | Dynamic 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>
}} /><Banner>
Info/warning/error/success message box.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'info' | 'warning' | 'error' | 'success' | required | Visual style |
title | string | Banner title | |
showIf | Condition | Conditional 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.
| Prop | Type | Default | Description |
|---|---|---|---|
showIf | Condition | Conditional visibility |
Navigation & UI Atoms
These components control the form chrome. Place them at the top level inside <Form>, outside sections.
<Header> / <Footer>
Container for top/bottom content. Wrap atom components inside.
| Prop | Type | Default | Description |
|---|---|---|---|
showIf | Condition | Conditional visibility |
<SidebarNav> / <StepperNav>
Section navigation widgets.
| Prop | Type | Default | Description |
|---|---|---|---|
showLabels | boolean | true | Show section labels |
showIf | Condition | Conditional visibility |
<FormContent>
Placeholder for the active section content area. Used when building custom layouts.
<FormTitle>
Renders the form title.
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | Override title (defaults to Form's title prop) |
<ProgressBar>
Form completion progress indicator.
| Prop | Type | Default | Description |
|---|---|---|---|
label | boolean | Show percentage label | |
scope | 'form' | 'section' | 'form' | Progress scope |
sectionId | string | Specific section to track |
<AutoSaveStatus>
Auto-save status indicator (shows "Saving...", "Saved", etc.).
<NavActions>
Composite back/next/submit button bar.
| Prop | Type | Default | Description |
|---|---|---|---|
submitLabel | string | "Submit" | Submit button text |
saveLabel | string | Save button text (only shown if provided) | |
backLabel | string | "Back" | Back button text |
nextLabel | string | "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.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | required | Slot identifier |
showIf | Condition | Conditional 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 theseOperators
| Operator | Description | Example |
|---|---|---|
eq | Equals | { field: { eq: "value" } } |
ne | Not equals | { field: { ne: "" } } |
gt | Greater than | { age: { gt: 17 } } |
gte | Greater than or equal | { age: { gte: 18 } } |
lt | Less than | { score: { lt: 100 } } |
lte | Less than or equal | { score: { lte: 100 } } |
in | Value in array | { role: { in: ["admin", "editor"] } } |
nin | Value not in array | { role: { nin: ["guest"] } } |
Logical Operators
| Operator | Description | Example |
|---|---|---|
$or | Any condition matches | { $or: [{ country: "US" }, { country: "CA" }] } |
$not | Condition 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
| Function | Description | Example |
|---|---|---|
SUM | Sum of values | SUM({{a}}, {{b}}, {{c}}) |
COUNT | Count of non-empty values | COUNT({{a}}, {{b}}, {{c}}) |
MIN | Minimum value | MIN({{a}}, {{b}}) |
MAX | Maximum value | MAX({{a}}, {{b}}) |
IF | Conditional value | IF({{qty}} > 0, {{qty}} * {{price}}, 0) |
ROUND | Round to N decimals | ROUND({{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
| Token | Description | Example |
|---|---|---|
{{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
- Special tokens (
{{index}},{{index1}}) - Current repeat item scope values
- Root form values
- 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
| Format | Pattern | Example |
|---|---|---|
ssn | ###-##-#### | 123-45-6789 |
ein | ##-####### | 12-3456789 |
phone | (###) ###-#### | (555) 123-4567 |
zip | ##### | 90210 |
zip4 | #####-#### | 90210-1234 |
routingNumber | ######### | 021000021 |
itin | ###-##-#### | 900-70-0000 |
canadianPostal | A#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
Every Question needs a unique
idwithin the form. IDs are used for data binding, conditions, and formulas.Use
Rowfor side-by-side layout, not CSS. The renderer handles responsive behavior.Sections with
showIfare automatically skipped in navigation when hidden.Options must have
valueandlabel:tsx// Correct options={[{ value: 'us', label: 'United States' }]} // Wrong options={['us', 'ca']}Don't combine
requiredandrequiredIfon the same field.requiredIftakes precedence when present.Repeat children define the template for one item. Don't wrap them in their own
Section.Formula fields are automatically read-only. Don't add
disabledor similar props.ContentwithrenderreceivesFormStateContextwhich has:getAnswer(fieldId)- get a field valuesetAnswer(fieldId, value)- set a field valueanswers- all current answers
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'Connected mode (embed with API key) handles everything automatically - no
onSubmitneeded:tsx<Form id="my-form" version="1.0" apiKey="fdk_live_xxxxx"> ... </Form>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" />Custom validation messages override defaults:
tsx<Question id="age" type="number" required min={18} validationMessage="You must be at least 18 years old" />