BlogWork With Me →
Back to Blog

Build a Multi-Step Survey in 5 Minutes with FieldCraft

TL;DR — Define a multi-step survey as a single JSON schema. FieldCraft handles navigation, progress tracking, per-section validation, conditional fields, and draft persistence. You write zero form state code.

What We're Building

A three-step customer feedback survey with:

  • Step 1: Contact info with email validation
  • Step 2: Product feedback with ratings and conditional follow-up
  • Step 3: Final comments with optional contact consent

Progress bar, back navigation, per-section validation, and conditional fields — all defined in one JSON schema. No useState, no form libraries, no validation wiring.

Setup

Install FieldCraft's core engine and React renderer:

npm install @squaredr/fieldcraft-core @squaredr/fieldcraft-react

The Schema

FieldCraft forms are defined as JSON schemas. A multi-step form uses the sections array — each section becomes one step. Let's build the schema piece by piece.

Form metadata and settings

The top-level object defines the form's identity and UX behavior:

import type { FormEngineSchema } from "@squaredr/fieldcraft-core";

const surveySchema: FormEngineSchema = {
id: "customer-survey",
version: "1.0.0",
title: "Customer Feedback Survey",
description: "Help us improve — takes about 2 minutes.",
settings: {
showProgress: true,
progressStyle: "steps", // "steps", "bar", or "percentage"
navigation: {
showBack: true, // Show back button
allowSkip: false, // Can't skip sections
},
},
submitAction: { type: "callback" },
sections: [ /* ...sections go here */ ],
};

Change progressStyle to "percentage" to show "67% complete" instead of "Step 2 of 3". Change "bar" for a visual progress bar. All three styles work with zero additional code.

Section 1 — Contact info

Each object in the sections array becomes one step in the multi-step flow. Users can't advance until all required fields in the current section pass validation.

{
id: "contact",
title: "About You",
description: "So we know who's giving feedback",
questions: [
{
id: "name",
type: "short_text",
label: "Your Name",
required: true,
placeholder: "Jane Smith",
},
{
id: "email",
type: "email",
label: "Email Address",
required: true,
placeholder: "jane@company.com",
helpText: "We'll only contact you about your feedback",
},
{
id: "role",
type: "single_select",
label: "Your Role",
required: true,
options: [
{ label: "Developer", value: "developer" },
{ label: "Designer", value: "designer" },
{ label: "Product Manager", value: "pm" },
{ label: "Executive", value: "exec" },
{ label: "Other", value: "other" },
],
},
],
}

Three fields: a text input, an email input with built-in format validation, and a dropdown. All required — the user must fill them in before moving to Step 2.

Section 2 — Product feedback

{
id: "feedback",
title: "Your Feedback",
description: "Tell us what you think",
questions: [
{
id: "satisfaction",
type: "rating",
label: "How satisfied are you with the product?",
required: true,
},
{
id: "recommend",
type: "nps",
label: "How likely are you to recommend us?",
required: true,
},
{
id: "best_feature",
type: "single_select",
label: "What do you like most?",
required: false,
options: [
{ label: "Ease of use", value: "ease" },
{ label: "Performance", value: "performance" },
{ label: "Documentation", value: "docs" },
{ label: "Support", value: "support" },
{ label: "Pricing", value: "pricing" },
],
},
{
id: "pain_point",
type: "long_text",
label: "What's your biggest pain point?",
required: false,
placeholder: "Tell us what frustrates you...",
validation: [{ type: "maxLength", value: 500 }],
},
],
}

This section mixes field types: a star rating, an NPS score (0-10), a dropdown, and a long text area with a 500-character limit. The validation array on pain_point adds the maxLength constraint.

Section 3 — Final thoughts with conditional logic

{
id: "closing",
title: "Final Thoughts",
description: "Anything else?",
questions: [
{
id: "improvement",
type: "long_text",
label: "If you could change one thing, what would it be?",
required: false,
placeholder: "Your suggestion...",
validation: [{ type: "maxLength", value: 1000 }],
},
{
id: "can_contact",
type: "boolean",
label: "Can we follow up with you about your feedback?",
required: false,
},
{
id: "preferred_contact",
type: "single_select",
label: "How should we reach you?",
required: true,
options: [
{ label: "Email", value: "email" },
{ label: "Phone", value: "phone" },
{ label: "Slack", value: "slack" },
],
showIf: {
field: "can_contact",
operator: "eq",
value: true,
},
},
],
}

Notice the showIf on preferred_contact — this field only appears when the user toggles can_contact to true. That's conditional logic, covered in detail below.

That's the entire form. Three sections, nine fields, conditional logic, validation rules, progress tracking, and back navigation — all from a single JSON object. No useState, no event handlers, no validation wiring.

Validation

Required fields get validated automatically. For custom rules, add a validation array:

{
id: "pain_point",
type: "long_text",
label: "What's your biggest pain point?",
validation: [{ type: "maxLength", value: 500 }],
}

Built-in validators include minLength, maxLength, min, max, pattern (regex), and email. The email validator checks for a valid TLD — user@example.c is rejected, user@example.co is accepted.

Errors display on blur, not on every keystroke. Users finish typing before seeing validation feedback.

Conditional Fields

The showIf property controls field visibility based on other field values:

{
id: "preferred_contact",
type: "single_select",
label: "How should we reach you?",
showIf: {
field: "can_contact",
operator: "eq",
value: true,
},
}

The "preferred contact" field only appears when the user toggles "Can we follow up?" to yes. The engine re-evaluates conditions on every value change.

Automatic cleanup: if a user fills in the conditional field, then toggles the parent back to "no", the hidden field's value is cleared. You never submit stale data from hidden fields.

Available operators

You can use any of these operators in showIf conditions and nest them with AND/OR groups for complex logic:

OperatorMeaning
eq / neqEquals / not equals
gt / ltGreater than / less than
gte / lteGreater than or equal / less than or equal
contains / notContainsString or array contains value
startsWith / endsWithString starts or ends with value
in / notInValue is in / not in a list
isEmpty / isNotEmptyValue is empty / not empty

Rendering

One component, one prop for the schema, one callback for submission:

import { FormEngineRenderer } from "@squaredr/fieldcraft-react";
import "@squaredr/fieldcraft-react/styles.css";

export default function SurveyPage() {
const handleSubmit = (response) => {
console.log(response.values);
// POST to your API, save to database, etc.
};

return (
<FormEngineRenderer
schema={surveySchema}
onSubmit={handleSubmit}
/>
);
}

The response.values object contains all field values keyed by field ID:

{
"name": "Jane Smith",
"email": "jane@company.com",
"role": "developer",
"satisfaction": 4,
"recommend": 9,
"best_feature": "ease",
"pain_point": "Documentation could be more detailed",
"improvement": "Add more code examples",
"can_contact": true,
"preferred_contact": "email"
}

Draft Persistence

Long surveys risk abandonment. FieldCraft auto-saves progress to localStorage by default. If a user closes the tab and comes back within 72 hours, their answers are restored — including which section they were on.

Zero configuration. Draft persistence is on by default. Users pick up exactly where they left off — same section, same answers.

Theming

Import a theme preset and pass it to the theme prop:

import { darkPreset } from '@squaredr/fieldcraft-react'

<FormEngineRenderer
schema={surveySchema}
theme={darkPreset}
onSubmit={handleSubmit}
/>

Built-in presets: cleanPreset, darkPreset, modernPreset, highContrastPreset, clinicalPreset, playfulPreset. Or pass a custom FormEngineTheme object for full control over colors, spacing, borders, and typography.

Using a Pre-Built Template

Don't want to write the schema from scratch? FieldCraft ships 16 free templates:

npm install @squaredr/fieldcraft-templates-free
import { feedbackSurveySchema } from "@squaredr/fieldcraft-templates-free";

<FormEngineRenderer
schema={feedbackSurveySchema}
onSubmit={handleSubmit}
/>

That gives you a three-section feedback survey with NPS, Likert scales, ratings, conditional follow-up, and progress tracking — zero configuration.

Browse all 16 templates on npm or check the template catalog.

What's Next

This tutorial covers the basics. FieldCraft also supports:

  • Computed fields — calculate values from other fields using expressions
  • Custom validators — register your own validation logic
  • Custom field types — build and register field components
  • Storage adapters — save responses to Postgres, Supabase, or any webhook endpoint
  • File uploads — with drag-and-drop and preview

Full docs at squaredr.tech/products/fieldcraft/docs.


Links