DEV Community

Cover image for I got tired of rebuilding multi-step form logic from scratch, so I built FormFlow
Shane.
Shane.

Posted on

I got tired of rebuilding multi-step form logic from scratch, so I built FormFlow

Every project I've worked on that needed a multi-step form ended up the same way: a tangle of useReducer, scattered localStorage calls, and validation logic duplicated across steps. The fifth time I found myself writing the same "which step are we on?" state machine, I stopped and built something reusable.

That thing is FormFlow — a headless React hook for multi-step forms with conditional branching, Zod validation, and optional persistence baked in.

The problem it solves

Multi-step forms sound simple until you're deep in them:

  • Conditional steps — "Show the salary step only if they're applying full-time"
  • Partial saves — users abandon halfway through; you need to restore their progress
  • Per-step validation — Zod schemas that gate the Next button before advancing
  • Back navigation — going back shouldn't wipe the work they already did

All of this is solvable, but it's a lot of boilerplate you rewrite every time.

What FormFlow looks like

import { useFormFlow } from '@shngffrd/formflow'
import { z } from 'zod'

const steps = [
  {
    id: 'personal',
    title: 'Personal Info',
    schema: z.object({
      name: z.string().min(2),
      email: z.string().email(),
    }),
  },
  {
    id: 'employment',
    title: 'Employment',
    schema: z.object({ type: z.enum(['full-time', 'contract']) }),
  },
  {
    id: 'salary',
    title: 'Salary',
    condition: (data) => data.employment?.type === 'full-time',
    schema: z.object({ expected: z.number().min(0) }),
  },
]

function ApplicationForm() {
  const { state, actions, register } = useFormFlow({ steps, persist: 'application-v1' })

  return (
    <FormShell state={state} actions={actions}>
      <CurrentStep state={state} register={register} />
    </FormShell>
  )
}
Enter fullscreen mode Exit fullscreen mode

The salary step only appears when the condition returns true — and it recalculates live as the user changes their employment type. No manual if chains. No step index arithmetic.

Key features

Conditional branching — Steps include/exclude themselves based on current form data. The active step list updates automatically.

Zod validation per step — Pass a schema; the hook validates on next() and surfaces field errors. No separate form library needed.

Persistence — Pass a persist key and the hook saves/restores from localStorage. Works across page refreshes, tab closes, everything.

Headless — FormFlow owns zero UI. You bring your own components, design system, whatever. The hook just gives you state + actions.

TypeScript-first — Full generics. Your data object is typed end-to-end based on the schemas you pass.

Installing

npm install @shngffrd/formflow
# or
pnpm add @shngffrd/formflow
Enter fullscreen mode Exit fullscreen mode

Try the live demo

The demo is a realistic software engineer application form — 6 steps, conditional branching, Zod validation, and persistence so you can refresh and pick up where you left off.

👉 formflow.shngffrd.com

Full docs (including the API reference and a getting-started guide) live at:

👉 formflow.shngffrd.com/docs/getting-started

Source

It's fully open source — MIT licensed.

github.com/shngffrddev/formflow

PRs and issues welcome. If you run into something or want a feature, open an issue.


Happy to answer questions in the comments — especially if you've hit the same multi-step form pain and solved it differently. Always curious what patterns people land on.

Top comments (0)