[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"search-suggestions":44,"article-react-multi-step-form-the-complete-guide-with-cheat-sheet":92,"related-react-multi-step-form-the-complete-guide-with-cheat-sheet":731,"code:tsx:true:1c7sji3":857,"code:plain:true:13qqbcx":858,"code:tsx:true:18qx4kl":859,"code:bash:true:5d1jmr":860,"code:tsx:true:cguykl":861,"code:tsx:true:s6vpw":862,"code:tsx:true:o62lci":863,"code:tsx:true:ghx7vd":864,"code:tsx:true:vgw8yb":865,"code:tsx:true:qx25np":866,"code:diff:true:16kjuxc":867,"code:tsx:true:12405uz":868,"code:css:true:8339rg":869,"code:tsx:true:zbq3bk":870,"code:css:true:1xhzdiu":871,"code:tsx:true:skscz8":872,"code:css:true:xqljk5":873,"code:tsx:true:10n966l":874,"code:tsx:true:bc3l6h":875,"code:tsx:true:itpzt9":876,"code:tsx:true:ed3wr2":877,"comments-01a05242-45d4-724a-b453-6b7432196185":878},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,[45,49,53,57,61,65,69,73,77,81,85,89],{"slug":46,"name":47,"articles":48},"webdev","Webdev",73,{"slug":50,"name":51,"articles":52},"frontend","Frontend",64,{"slug":54,"name":55,"articles":56},"javascript","Javascript",61,{"slug":58,"name":59,"articles":60},"css","Css",29,{"slug":62,"name":63,"articles":64},"tutorial","Tutorial",13,{"slug":66,"name":67,"articles":68},"typescript","Typescript",11,{"slug":70,"name":71,"articles":72},"performance","Performance",9,{"slug":74,"name":75,"articles":76},"react","React",7,{"slug":78,"name":79,"articles":80},"grammar","Grammar",6,{"slug":82,"name":83,"articles":84},"node","Node",5,{"slug":86,"name":87,"articles":88},"programming","Programming",4,{"slug":90,"name":91,"articles":88},"html","Html",{"id":93,"slug":94,"title":95,"subtitle":96,"excerpt":97,"coverUrl":98,"locale":13,"readingMinutes":99,"publishedAt":100,"viewCount":56,"likeCount":19,"commentCount":19,"author":101,"vertical":107,"topic":105,"tags":108,"_count":115,"playground":105,"body":116,"bodyMd":723,"seo":724,"translationGroupId":725,"series":105,"podcastUrl":105,"verticalId":5,"thread":726,"assessments":728,"translations":729,"quiz":105},"01a05242-45d4-724a-b453-6b7432196185","react-multi-step-form-the-complete-guide-with-cheat-sheet","React Multi-Step Form: The Complete Guide (with Cheat Sheet)","You already know how to fix that. You also know that the moment the step index stops living in `useState`, the browser back button, the per-step validation, and the focus ring all become your problem too.","Build a React multi-step form that validates per step, survives a refresh, and stays accessible — the mental model, the traps, and a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Freact-form-wizard-component-v2.png",20,"2026-08-30T10:41:23.244Z",{"id":102,"name":103,"username":104,"avatarUrl":105,"headline":106},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa",null,"Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},[109,110,111,113],{"slug":50,"name":51,"color":105},{"slug":74,"name":75,"color":105},{"slug":112,"name":112,"color":105},"next",{"slug":114,"name":114,"color":105},"package",{"assessments":19},{"blocks":117,"version":722},[118,121,125,132,135,139,142,145,148,152,155,163,166,169,172,186,189,192,198,201,209,212,215,218,256,259,262,265,268,271,274,279,282,285,289,292,295,298,301,304,310,313,316,323,326,329,332,337,341,344,347,354,357,360,363,366,369,373,376,379,382,386,389,393,396,400,403,406,409,413,416,420,423,426,429,434,437,440,443,447,450,454,457,460,463,467,470,473,476,479,482,486,489,492,495,499,502,506,509,513,516,519,522,526,529,532,536,539,542,546,549,552,555,564,567,570,573,576,579,582,585,588,591,594,597,600,603,606,609,684,687,691,694,702,705,708,710,713,716],{"id":119,"type":120},"b1","divider",{"id":122,"html":123,"type":124},"b2","\u003Cp>title: &quot;React Multi-Step Form: The Complete Guide (with Cheat Sheet)&quot;\ndescription: &quot;Build a React multi-step form that validates per step, survives a refresh, and stays accessible — the mental model, the traps, and a copy-paste cheat sheet.&quot;\ntags: [react, typescript, tutorial, webdev]\npublished: true\nseries: &quot;React Toolbox&quot;\npackage: react-form-wizard-component\nversion: 2.0.0\npreviousVersion: 1.2.1\necosystem: react\ntagline: &quot;Accessible multi-step form wizard for React 17, 18 and 19 — zero dependencies, styled or headless.&quot;\nhighlights:\u003C\u002Fp>","paragraph",{"id":126,"type":127,"items":128,"ordered":18},"b3","list",[129,130,131],"New default look, and dark mode that actually works","Tailwind token bridge and utility preset","Same API — legacy skin is one opt-in line\nlinkedin: |\n  Multi-step forms look like two useState calls until you ship one.",{"id":133,"html":134,"type":124},"b4","\u003Cp>  • A wizard is really three things wearing one component&#39;s clothes: a cursor, a shared data object, and a per-step gate. Name them and every bug has an address.\n  • Per-step validation does NOT mean four separate forms — one schema, one form, a gate that picks its own fields.\n  • An embedded component should never read prefers-color-scheme. A light page on a dark-mode laptop gets a dark widget on white.\u003C\u002Fp>",{"id":136,"html":137,"text":137,"type":138,"level":31},"b5","Full write-up — the mental model, the traps, and a copy-paste cheat sheet 👇","heading",{"id":140,"html":141,"type":124},"b6","\u003Cp>The signup form has four steps. Step three uploads a passport photo and takes eight seconds. A user reaches step four, fat-fingers ⌘R, and lands back on step one with an empty form.\u003C\u002Fp>",{"id":143,"html":144,"type":124},"b7","\u003Cp>You already know how to fix that. You also know that the moment the step index stops living in \u003Ccode>useState\u003C\u002Fcode>, the browser back button, the per-step validation, and the focus ring all become your problem too.\u003C\u002Fp>",{"id":146,"html":147,"type":124},"b8","\u003Cp>Multi-step forms look like two \u003Ccode>useState\u003C\u002Fcode> calls until you ship one.\u003C\u002Fp>",{"id":149,"html":150,"text":151,"type":138,"level":31},"b9","What you&#39;ll learn","What you'll learn",{"id":153,"html":154,"type":124},"b10","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":156,"type":127,"items":157,"ordered":18},"b11",[158,159,160,161,162],"Model any React multi-step form as three separate things — a cursor, shared data, and a per-step gate — and know which one a given bug belongs to","Validate one step&#39;s fields without splitting your form into four forms","Keep wizard state across a refresh and a shared link, without a router","Explain why an embedded component should ignore \u003Ccode>prefers-color-scheme\u003C\u002Fcode>","Ship the whole thing with correct ARIA roles, focus movement, and keyboard support",{"id":164,"html":165,"text":165,"type":138,"level":31},"b12","Who this is for",{"id":167,"html":168,"type":124},"b13","\u003Cp>You&#39;ve built a React form with controlled inputs and reached for a validation library at least once. TypeScript is used throughout but nothing here depends on it.\u003C\u002Fp>",{"id":170,"html":171,"text":171,"type":138,"level":31},"b14","Table of contents",{"id":173,"type":127,"items":174,"ordered":18},"b15",[175,176,177,178,179,180,181,182,183,184,185],"\u003Ca href=\"#why-multi-step-forms-are-harder-than-they-look\">Why multi-step forms are harder than they look\u003C\u002Fa>","\u003Ca href=\"#what-people-build-today\">What people build today\u003C\u002Fa>","\u003Ca href=\"#where-the-usual-options-actually-break\">Where the usual options actually break\u003C\u002Fa>","\u003Ca href=\"#the-mental-model\">The mental model\u003C\u002Fa>","\u003Ca href=\"#enter-react-form-wizard-component\">Enter react-form-wizard-component\u003C\u002Fa>","\u003Ca href=\"#the-api-in-four-stages\">The API, in four stages\u003C\u002Fa>","\u003Ca href=\"#whats-new-in-v20\">What&#39;s new in v2.0\u003C\u002Fa>","\u003Ca href=\"#three-recipes\">Three recipes\u003C\u002Fa>","\u003Ca href=\"#when-not-to-use-it\">When not to use it\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>",{"id":187,"html":188,"text":188,"type":138,"level":31},"b16","Why multi-step forms are harder than they look",{"id":190,"html":191,"type":124},"b17","\u003Cp>Here is the version everyone writes first, in full:\u003C\u002Fp>",{"id":193,"code":194,"type":195,"language":196,"highlight":197},"b18","function Signup() {\n  const [step, setStep] = useState(0);\n  const [data, setData] = useState({});\n\n  return (\n    \u003C>\n      {step === 0 && \u003CAccount data={data} onChange={setData} \u002F>}\n      {step === 1 && \u003CProfile data={data} onChange={setData} \u002F>}\n      {step === 2 && \u003CReview data={data} \u002F>}\n\n      \u003Cbutton onClick={() => setStep((s) => s - 1)} disabled={step === 0}>Back\u003C\u002Fbutton>\n      \u003Cbutton onClick={() => setStep((s) => s + 1)}>Next\u003C\u002Fbutton>\n    \u003C\u002F>\n  );\n}","code","tsx",[],{"id":199,"html":200,"type":124},"b19","\u003Cp>It works. It also ships five bugs, and each one costs about a day:\u003C\u002Fp>",{"id":202,"type":127,"items":203,"ordered":17},"b20",[204,205,206,207,208],"\u003Cstrong>Next always advances.\u003C\u002Fstrong> There is no gate. A user tabs past every required field and lands on Review with an empty object.","\u003Cstrong>A refresh is a total loss.\u003C\u002Fstrong> \u003Ccode>step\u003C\u002Fcode> and \u003Ccode>data\u003C\u002Fcode> are component state; ⌘R resets both.","\u003Cstrong>The browser back button leaves the page.\u003C\u002Fstrong> Users expect Back to mean &quot;previous step.&quot; Yours means &quot;goodbye.&quot;","\u003Cstrong>A screen reader is told nothing.\u003C\u002Fstrong> The panel swaps, the DOM changes, and the announcement queue stays empty. Focus is still sitting on the Next button that just disappeared and got re-rendered as a different button.","\u003Cstrong>You cannot link to step 3.\u003C\u002Fstrong> Support asks a customer to &quot;go to the payment step&quot; and has to describe the route in words.",{"id":210,"html":211,"type":124},"b21","\u003Cp>None of those are hard \u003Cem>individually\u003C\u002Fem>. The problem is that they are five different subsystems, and the naive version has no seam to hang any of them on.\u003C\u002Fp>",{"id":213,"html":214,"text":214,"type":138,"level":31},"b22","What people build today",{"id":216,"html":217,"type":124},"b23","\u003Cp>The honest survey, with versions checked against npm at the time of writing:\u003C\u002Fp>",{"id":219,"head":220,"rows":225,"type":255},"b24",[221,222,223,224],"Package","Latest","Last published","Shape",[226,231,236,241,246,250],[227,228,229,230],"\u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-hook-form\">\u003Ccode>react-hook-form\u003C\u002Fcode>\u003C\u002Fa>","7.87.0","Aug 2026","Form state. No opinion about steps at all.",[232,233,234,235],"\u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fformik\">\u003Ccode>formik\u003C\u002Fcode>\u003C\u002Fa>","2.4.9","Nov 2025","Same — form state, no cursor.",[237,238,239,240],"\u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@stepperize\u002Freact\">\u003Ccode>@stepperize\u002Freact\u003C\u002Fcode>\u003C\u002Fa>","7.0.0","Jun 2026","Headless, type-safe, actively maintained. Ships zero UI by design.",[242,243,244,245],"\u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-step-wizard\">\u003Ccode>react-step-wizard\u003C\u002Fcode>\u003C\u002Fa>","5.3.11","Aug 2024","Styled component, quiet for two years.",[247,238,248,249],"\u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-multistep\">\u003Ccode>react-multistep\u003C\u002Fcode>\u003C\u002Fa>","Feb 2026","Styled component, opinionated markup.",[251,252,253,254],"\u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-albus\">\u003Ccode>react-albus\u003C\u002Fcode>\u003C\u002Fa>","2.0.0","Jun 2022","Router-flavoured wizard. Three runtime deps.","table",{"id":257,"html":258,"type":124},"b25","\u003Cp>Two of these deserve a genuine defence.\u003C\u002Fp>",{"id":260,"html":261,"type":124},"b26","\u003Cp>\u003Cstrong>react-hook-form is the right answer to a different question.\u003C\u002Fstrong> It is the best form-state library in the React ecosystem and it is not trying to be a wizard. Nothing below argues against using it — the recipes actively use it.\u003C\u002Fp>",{"id":263,"html":264,"type":124},"b27","\u003Cp>\u003Cstrong>@stepperize\u002Freact is good, current, and well typed.\u003C\u002Fstrong> If your design system already owns every pixel and you want nothing but a state machine, use it. It ships no markup on purpose, which means you write the rail, the progress bar, the buttons, and all of the \u003Ca href=\"https:\u002F\u002Fwww.w3.org\u002FWAI\u002FARIA\u002Fapg\u002Fpatterns\u002Ftabs\u002F\">ARIA tabs wiring\u003C\u002Fa> yourself. That is a feature when you have a design system, and a week of work when you don&#39;t.\u003C\u002Fp>",{"id":266,"html":267,"text":267,"type":138,"level":31},"b28","Where the usual options actually break",{"id":269,"html":270,"type":124},"b29","\u003Cp>Three concrete gaps, not vibes.\u003C\u002Fp>",{"id":272,"html":273,"type":124},"b30","\u003Cp>\u003Cstrong>The peer range that stops the install.\u003C\u002Fstrong> \u003Ccode>react-multistep@7.0.0\u003C\u002Fcode> declares \u003Ccode>peerDependencies\u003C\u002Fcode> as \u003Ccode>{ &quot;react&quot;: &quot;18.3.1&quot; }\u003C\u002Fcode> — an exact version, not a range. On a React 19 app, npm refuses:\u003C\u002Fp>",{"id":275,"code":276,"type":195,"language":277,"highlight":278},"b31","npm error code ERESOLVE\nnpm error ERESOLVE unable to resolve dependency tree\nnpm error Found: react@19.2.0\nnpm error Could not resolve dependency:\nnpm error peer react@\"18.3.1\" from react-multistep@7.0.0\nnpm error Fix the upstream dependency conflict, or retry\nnpm error this command with --force or --legacy-peer-deps","plain",[],{"id":280,"html":281,"type":124},"b32","\u003Cp>You can \u003Ccode>--legacy-peer-deps\u003C\u002Fcode> past it. You are then running a component whose author has not tested your React version.\u003C\u002Fp>",{"id":283,"html":284,"type":124},"b33","\u003Cp>\u003Cstrong>Headless means you own the accessibility.\u003C\u002Fstrong> Here is the minimum the tabs pattern asks for, and none of it comes free:\u003C\u002Fp>",{"id":286,"code":287,"type":195,"language":196,"highlight":288},"b34","\u003Cdiv role=\"tablist\" aria-label=\"Signup steps\">\n  \u003Cbutton\n    role=\"tab\"\n    id=\"tab-account\"\n    aria-selected={step === 0}\n    aria-controls=\"panel-account\"\n    tabIndex={step === 0 ? 0 : -1}   \u002F\u002F roving tabindex, not 0 on every tab\n  >\n    Account\n  \u003C\u002Fbutton>\n  {\u002F* …and arrow-key handling, and Home\u002FEnd, and focus movement on change, *\u002F}\n  {\u002F* and an aria-live region so the change is announced at all *\u002F}\n\u003C\u002Fdiv>",[],{"id":290,"html":291,"type":124},"b35","\u003Cp>That is a real day of work, and it is the part that gets cut when the sprint is tight.\u003C\u002Fp>",{"id":293,"html":294,"type":124},"b36","\u003Cp>\u003Cstrong>Styled-but-stale means you inherit the CSS.\u003C\u002Fstrong> \u003Ccode>react-albus\u003C\u002Fcode> was last published in June 2022 and still pulls in three runtime dependencies (\u003Ccode>history\u003C\u002Fcode>, \u003Ccode>hoist-non-react-statics\u003C\u002Fcode>, \u003Ccode>prop-types\u003C\u002Fcode>). A styled component that has stopped shipping is one you restyle with \u003Ccode>!important\u003C\u002Fcode>, which is the sound of a dependency that has stopped helping.\u003C\u002Fp>",{"id":296,"html":297,"text":297,"type":138,"level":31},"b37","The mental model",{"id":299,"html":300,"type":124},"b38","\u003Cp>This is the section worth keeping even if you install nothing.\u003C\u002Fp>",{"id":302,"html":303,"type":124},"b39","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> a multi-step form is three independent things wearing one component&#39;s clothes.\u003C\u002Fp>",{"id":305,"type":127,"items":306,"ordered":17},"b40",[307,308,309],"\u003Cstrong>A cursor\u003C\u002Fstrong> — which step is showing, plus the furthest step reached. Two numbers.","\u003Cstrong>Shared data\u003C\u002Fstrong> — one object every step reads and writes. Not one state per step.","\u003Cstrong>A gate per step\u003C\u002Fstrong> — a pure function answering one question: \u003Cem>may the user leave this step?\u003C\u002Fem> It returns \u003Ccode>true\u003C\u002Fcode>, or a string that is both &quot;no&quot; and the reason.",{"id":311,"html":312,"type":124},"b41","\u003Cp>Everything else is a \u003Cem>projection\u003C\u002Fem> of those three. The progress bar is a projection of the cursor. Persistence is serialising the data. A deep link is the cursor in the URL. Focus management is a side effect of the cursor changing.\u003C\u002Fp>",{"id":314,"html":315,"type":124},"b42","\u003Cp>The payoff is diagnostic. Every wizard bug now has an address:\u003C\u002Fp>",{"id":317,"type":127,"items":318,"ordered":18},"b43",[319,320,321,322],"A lost draft is a \u003Cstrong>data\u003C\u002Fstrong> problem.","A Next button that won&#39;t move is a \u003Cstrong>gate\u003C\u002Fstrong> problem.","A back button that exits the page is a \u003Cstrong>cursor\u003C\u002Fstrong> problem.","Steps that unlock when they shouldn&#39;t is a \u003Cstrong>cursor\u003C\u002Fstrong> problem too — specifically the &quot;furthest reached&quot; number.",{"id":324,"html":325,"type":124},"b44","\u003Cp>Note what the gate is \u003Cem>not\u003C\u002Fem>: it is not a submit handler. It runs on every render, so it must be pure and cheap, and it must never set state. Get that wrong and you have an infinite render loop rather than a validation bug.\u003C\u002Fp>",{"id":327,"html":328,"text":328,"type":138,"level":31},"b45","Enter react-form-wizard-component",{"id":330,"html":331,"type":124},"b46","\u003Cp>The three pieces above are exactly what \u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-form-wizard-component\">\u003Ccode>react-form-wizard-component\u003C\u002Fcode>\u003C\u002Fa> implements, and \u003Cstrong>v2.0.0\u003C\u002Fstrong> landed on npm at the end of August 2026.\u003C\u002Fp>",{"id":333,"code":334,"type":195,"language":335,"highlight":336},"b47","npm install react-form-wizard-component","bash",[],{"id":338,"code":339,"type":195,"language":196,"highlight":340},"b48","import FormWizard from \"react-form-wizard-component\";\nimport \"react-form-wizard-component\u002Fstyles.css\";\n\nexport default function Signup() {\n  return (\n    \u003CFormWizard onComplete={(data) => console.log(data)}>\n      \u003CFormWizard.TabContent title=\"Account\">\n        \u003Cinput placeholder=\"Email\" \u002F>\n      \u003C\u002FFormWizard.TabContent>\n      \u003CFormWizard.TabContent title=\"Profile\">\n        \u003Cinput placeholder=\"Full name\" \u002F>\n      \u003C\u002FFormWizard.TabContent>\n      \u003CFormWizard.TabContent title=\"Review\">\n        \u003Cp>Looks good?\u003C\u002Fp>\n      \u003C\u002FFormWizard.TabContent>\n    \u003C\u002FFormWizard>\n  );\n}",[],{"id":342,"html":343,"type":124},"b49","\u003Cp>That is a working wizard with a progress bar, arrow-key and Home\u002FEnd navigation, swipe on touch, focus movement, and live-region announcements.\u003C\u002Fp>",{"id":345,"html":346,"type":124},"b50","\u003Cp>Honestly, in one sentence: \u003Cstrong>a styled component and a headless hook from the same package — 5.85 kB minified and brotlied, zero runtime dependencies, React 17, 18 and 19.\u003C\u002Fstrong> Those size numbers are the ones CI measures with \u003Ccode>size-limit\u003C\u002Fcode> on every commit, not a bundlephobia guess; the stylesheet is another 1.38 kB.\u003C\u002Fp>",{"id":348,"type":127,"items":349,"ordered":18},"b51",[350,351,352,353],"📦 npm: \u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-form-wizard-component\">https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-form-wizard-component\u003C\u002Fa>","🐙 GitHub: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\u002Freact-form-wizard\">https:\u002F\u002Fgithub.com\u002Fparsajiravand\u002Freact-form-wizard\u003C\u002Fa>","📚 Docs: \u003Ca href=\"https:\u002F\u002Freact-form-wizard-component-document.netlify.app\u002Fdocs\">https:\u002F\u002Freact-form-wizard-component-document.netlify.app\u002Fdocs\u003C\u002Fa>","▶️ Live demos: \u003Ca href=\"https:\u002F\u002Freact-form-wizard-component-document.netlify.app\u002Fdocs\u002Fdemos\">https:\u002F\u002Freact-form-wizard-component-document.netlify.app\u002Fdocs\u002Fdemos\u003C\u002Fa> · \u003Ca href=\"https:\u002F\u002Freact-form-wizard-component-document.netlify.app\u002Fdocs\u002Fplayground\">interactive playground\u003C\u002Fa>",{"id":355,"html":356,"text":356,"type":138,"level":31},"b52","The API, in four stages",{"id":358,"html":359,"text":359,"type":138,"level":43},"b53","Stage 1 — children, when the steps are static",{"id":361,"html":362,"type":124},"b54","\u003Cp>The example above. Each \u003Ccode>&lt;FormWizard.TabContent&gt;\u003C\u002Fcode> is a step; \u003Ccode>title\u003C\u002Fcode> and \u003Ccode>icon\u003C\u002Fcode> are optional.\u003C\u002Fp>",{"id":364,"html":365,"type":124},"b55","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> the children API owns the cursor for you and nothing else. It is the right choice only while every user sees every step.\u003C\u002Fp>",{"id":367,"html":368,"text":368,"type":138,"level":43},"b56","Stage 2 — schema, when steps depend on data",{"id":370,"code":371,"type":195,"language":196,"highlight":372},"b57","import FormWizard, { type FormWizardSchema } from \"react-form-wizard-component\";\n\nconst schema: FormWizardSchema = {\n  initialData: { plan: \"basic\" },\n  steps: [\n    { id: \"plan\", title: \"Plan\", content: \u003CPlanPicker \u002F> },\n    {\n      id: \"billing\",\n      title: \"Billing\",\n      \u002F\u002F The step disappears from the rail entirely when this is false.\n      condition: ({ data }) => data.plan === \"premium\",\n      content: \u003CBillingFields \u002F>,\n    },\n    {\n      id: \"review\",\n      title: \"Review\",\n      \u002F\u002F content can be a function of the current data.\n      content: ({ data }) => \u003CReview data={data} \u002F>,\n    },\n  ],\n};\n\n\u003CFormWizard schema={schema} onComplete={(data) => submit(data)} \u002F>;",[],{"id":374,"html":375,"type":124},"b58","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>condition\u003C\u002Fcode> is evaluated against shared data, so branching is declarative. You never write \u003Ccode>steps.filter(...)\u003C\u002Fcode> and then fix the off-by-one in the progress bar.\u003C\u002Fp>",{"id":377,"html":378,"text":378,"type":138,"level":43},"b59","Stage 3 — the gate, and the adapters that write it for you",{"id":380,"html":381,"type":124},"b60","\u003Cp>The contract is deliberately tiny. Return \u003Ccode>true\u003C\u002Fcode> to allow navigation, a \u003Cstrong>string\u003C\u002Fstrong> to block it and supply the message, \u003Ccode>false\u003C\u002Fcode> to block silently:\u003C\u002Fp>",{"id":383,"code":384,"type":195,"language":196,"highlight":385},"b61","validate: ({ data }) => (data.accepted ? true : \"Please accept the terms\"),",[],{"id":387,"html":388,"type":124},"b62","\u003Cp>Two adapters produce that function from tools you already use. With Zod:\u003C\u002Fp>",{"id":390,"code":391,"type":195,"language":196,"highlight":392},"b63","import { z } from \"zod\";\nimport FormWizard, { zodValidator } from \"react-form-wizard-component\";\n\nconst account = z.object({\n  email: z.string().email(\"Enter a valid email\"),\n  password: z.string().min(8, \"At least 8 characters\"),\n});\n\n\u002F\u002F `pick` narrows the whole wizard data down to this step's slice.\nvalidate: zodValidator(account, { pick: [\"email\", \"password\"] });",[],{"id":394,"html":395,"type":124},"b64","\u003Cp>With react-hook-form, gating a step on a subset of one form&#39;s fields:\u003C\u002Fp>",{"id":397,"code":398,"type":195,"language":196,"highlight":399},"b65","import { useForm } from \"react-hook-form\";\nimport FormWizard, { hookFormValidator } from \"react-form-wizard-component\";\n\nconst form = useForm({ mode: \"onChange\" });\n\nvalidate: hookFormValidator(form, { fields: [\"email\", \"phone\"] });",[],{"id":401,"html":402,"type":124},"b66","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> this is what &quot;per-step validation&quot; should mean — \u003Cem>one\u003C\u002Fem> form, one schema, and a gate that only looks at its own fields. Not four forms glued together with a reducer.\u003C\u002Fp>",{"id":404,"html":405,"type":124},"b67","\u003Cp>Neither \u003Ccode>zod\u003C\u002Fcode> nor \u003Ccode>react-hook-form\u003C\u002Fcode> becomes a dependency. Both adapters are typed structurally: \u003Ccode>zodValidator\u003C\u002Fcode> accepts anything with a \u003Ccode>safeParse\u003C\u002Fcode> method, so Valibot and ArkType work too, and \u003Ccode>hookFormValidator\u003C\u002Fcode> reads \u003Ccode>formState.errors\u003C\u002Fcode> and nothing else — it never mutates form state, which is what keeps it safe to run on every render. Combine rules with \u003Ccode>composeValidators(...)\u003C\u002Fcode>; first failure wins.\u003C\u002Fp>",{"id":407,"html":408,"text":408,"type":138,"level":43},"b68","Stage 4 — headless, when you own the markup",{"id":410,"code":411,"type":195,"language":196,"highlight":412},"b69","import { useWizard } from \"react-form-wizard-component\";\n\nconst wizard = useWizard({\n  stepIds: [\"account\", \"profile\", \"review\"],\n  persist: { key: \"signup\" },\n  syncToUrl: true, \u002F\u002F writes ?step=2\n});\n\nwizard.currentStep;    \u002F\u002F 0-based cursor\nwizard.maxVisitedStep; \u002F\u002F the forward gate\nwizard.data;           \u002F\u002F shared data\nwizard.next();\nwizard.goToId(\"review\");\nwizard.updateData({ email }); \u002F\u002F merges",[],{"id":414,"html":415,"type":124},"b70","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>&lt;FormWizard \u002F&gt;\u003C\u002Fcode> is built on this hook. Choosing headless is not choosing a different library — the state machine is identical, you are only declining the markup and the stylesheet.\u003C\u002Fp>",{"id":417,"html":418,"text":419,"type":138,"level":31},"b71","What&#39;s new in v2.0","What's new in v2.0",{"id":421,"html":422,"type":124},"b72","\u003Cp>Here is the part worth knowing if you are already on 1.x.\u003C\u002Fp>",{"id":424,"html":425,"type":124},"b73","\u003Cp>\u003Cstrong>Every prop, method, hook and adapter is unchanged.\u003C\u002Fstrong> v2.0.0 is a major version because the \u003Cem>default appearance\u003C\u002Fem> changed, and a visual change to a component you already styled is a breaking change even when the types say otherwise. Semver&#39;s job is to warn you before the deploy, not after.\u003C\u002Fp>",{"id":427,"html":428,"type":124},"b74","\u003Cp>Keeping the exact v1 look is a two-line diff:\u003C\u002Fp>",{"id":430,"code":431,"type":195,"language":432,"highlight":433},"b75","  import \"react-form-wizard-component\u002Fstyles.css\";\n+ import \"react-form-wizard-component\u002Flegacy.css\";\n\n- \u003CFormWizard … \u002F>\n+ \u003CFormWizard variant=\"legacy\" … \u002F>","diff",[],{"id":435,"html":436,"text":436,"type":138,"level":43},"b76","Dark mode that follows the page, not the machine",{"id":438,"html":439,"type":124},"b77","\u003Cp>This is the most interesting decision in the release, and it generalises well beyond this package.\u003C\u002Fp>",{"id":441,"html":442,"type":124},"b78","\u003Cp>v1&#39;s stylesheet had no \u003Ccode>prefers-color-scheme\u003C\u002Fcode> rules at all — dark mode existed only as inline styles you hand-listed through a \u003Ccode>customDarkModeColor\u003C\u002Fcode> prop. The obvious fix is to add a \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@media\u002Fprefers-color-scheme\">\u003Ccode>prefers-color-scheme\u003C\u002Fcode>\u003C\u002Fa> block. That fix is wrong, and here is why:\u003C\u002Fp>",{"id":444,"html":445,"type":446},"b79","\u003Cp>\u003Cstrong>An embedded component cannot assume the OS preference describes the surface behind it.\u003C\u002Fstrong> A light-only marketing page, viewed on a laptop set to dark, would render a dark wizard on a white card.\u003C\u002Fp>\n","quote",{"id":448,"html":449,"type":124},"b80","\u003Cp>So the default (\u003Ccode>colorScheme=&quot;auto&quot;\u003C\u002Fcode>) reacts only to explicit signals from the host page: an ancestor carrying \u003Ccode>[data-theme=&quot;dark&quot;]\u003C\u002Fcode> or \u003Ccode>.dark\u003C\u002Fcode> — which is what Tailwind, next-themes, Docusaurus and Fumadocs all set — or the \u003Ccode>darkMode\u003C\u002Fcode> prop.\u003C\u002Fp>",{"id":451,"code":452,"type":195,"language":196,"highlight":453},"b81","\u003CFormWizard \u002F>                        {\u002F* auto: follows the page *\u002F}\n\u003CFormWizard colorScheme=\"system\" \u002F>   {\u002F* opt in to prefers-color-scheme *\u002F}\n\u003CFormWizard colorScheme=\"dark\" \u002F>     {\u002F* pin it *\u002F}",[],{"id":455,"html":456,"type":124},"b82","\u003Cp>If you own the whole page rather than a component inside someone else&#39;s, the media query is the right tool — I wrote about the \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fyoure-writing-dark-mode-colors-twice-light-dark-fixes-that-44hb\">\u003Ccode>light-dark()\u003C\u002Fcode> CSS function\u003C\u002Fa> for exactly that case. The rule of thumb: \u003Cstrong>the page may read the OS; a component should read the page.\u003C\u002Fstrong>\u003C\u002Fp>",{"id":458,"html":459,"text":459,"type":138,"level":43},"b83","The skin stopped painting inline styles",{"id":461,"html":462,"type":124},"b84","\u003Cp>v1 set colours as inline styles on the markers, the rail and the footer. Inline styles beat every selector you can write short of \u003Ccode>!important\u003C\u002Fcode>, which is why theming and dark mode were unreliable. In v2, all of it flows from CSS custom properties and state classes:\u003C\u002Fp>",{"id":464,"code":465,"type":195,"language":58,"highlight":466},"b85",".react-form-wizard {\n  --rfw-primary: #0e6f70;\n  --rfw-radius: 8px;\n}",[],{"id":468,"html":469,"type":124},"b86","\u003Cp>Your CSS wins now, with normal specificity. Each step also carries state classes — \u003Ccode>active\u003C\u002Fcode>, \u003Ccode>rfw-done\u003C\u002Fcode>, \u003Ccode>rfw-invalid\u003C\u002Fcode> — so you can style a completed or failed step directly.\u003C\u002Fp>",{"id":471,"html":472,"type":124},"b87","\u003Cp>And because the v1 skin moved into its own opt-in stylesheet, the default is \u003Cem>smaller\u003C\u002Fem> than 1.2.1 despite gaining a full dark palette: \u003Cstrong>1.38 kB\u003C\u002Fstrong> brotlied, down from 1.85 kB.\u003C\u002Fp>",{"id":474,"html":475,"text":475,"type":138,"level":43},"b88","Steps no longer turn red before the user does anything",{"id":477,"html":478,"type":124},"b89","\u003Cp>A genuine bug, and my favourite kind — the one that only shows up in a real app. \u003Ccode>showErrorOnTab\u003C\u002Fcode> defaulted to \u003Ccode>!isValid\u003C\u002Fcode>, so any step carrying a validator painted its marker red on first paint. A wizard whose first question was required looked broken on load: the user is accused before they have typed a character.\u003C\u002Fp>",{"id":480,"html":481,"type":124},"b90","\u003Cp>The error state now waits until the user has actually tried to leave the step:\u003C\u002Fp>",{"id":483,"code":484,"type":195,"language":196,"highlight":485},"b91","\u002F\u002F Internals, for the curious — a Set of step ids, added to on a blocked next().\nshowErrorOnTab={step.showErrorOnTab ?? (attemptedSteps.has(step.id) && !step.isValid)}",[],{"id":487,"html":488,"type":124},"b92","\u003Cp>An explicit \u003Ccode>showErrorOnTab\u003C\u002Fcode> still applies immediately.\u003C\u002Fp>",{"id":490,"html":491,"text":491,"type":138,"level":43},"b93","Tailwind, two ways",{"id":493,"html":494,"type":124},"b94","\u003Cp>Keep the bundled skin but adopt your theme — one import, no \u003Ccode>unstyled\u003C\u002Fcode>, Tailwind v4:\u003C\u002Fp>",{"id":496,"code":497,"type":195,"language":58,"highlight":498},"b95","@import \"tailwindcss\";\n@import \"react-form-wizard-component\u002Fstyles.css\";\n@import \"react-form-wizard-component\u002Ftailwind.css\";",[],{"id":500,"html":501,"type":124},"b96","\u003Cp>Or build it entirely from utility classes:\u003C\u002Fp>",{"id":503,"code":504,"type":195,"language":196,"highlight":505},"b97","import FormWizard, { tailwindPreset } from \"react-form-wizard-component\";\n\n\u003CFormWizard unstyled classNames={tailwindPreset()} schema={schema} \u002F>;",[],{"id":507,"html":508,"type":124},"b98","\u003Cp>One trap worth naming: the preset&#39;s classes live in \u003Ccode>node_modules\u003C\u002Fcode>, and Tailwind only emits classes it can \u003Cem>see\u003C\u002Fem>. Point it at the package or you will get an unstyled wizard and no error message:\u003C\u002Fp>",{"id":510,"code":511,"type":195,"language":58,"highlight":512},"b99","\u002F* Tailwind v4 *\u002F\n@source \"..\u002Fnode_modules\u002Freact-form-wizard-component\u002Fdist\u002F**\u002F*.js\";",[],{"id":514,"html":515,"type":124},"b100","\u003Cp>Every class in the preset is a literal string for that reason — nothing is built by template concatenation, because \u003Ccode>`bg-${colour}-600`\u003C\u002Fcode> is invisible to a scanner. The accent reads from \u003Ccode>bg-[var(--rfw-primary)]\u003C\u002Fcode>, so recolouring stays a one-line CSS change instead of a rebuild.\u003C\u002Fp>",{"id":517,"html":518,"text":518,"type":138,"level":31},"b101","Three recipes",{"id":520,"html":521,"text":521,"type":138,"level":43},"b102","1. Checkout — one form, three gates",{"id":523,"code":524,"type":195,"language":196,"highlight":525},"b103","const form = useForm\u003CCheckout>({ resolver: zodResolver(checkoutSchema), mode: \"onChange\" });\n\nconst schema: FormWizardSchema = {\n  steps: [\n    {\n      id: \"contact\", title: \"Contact\",\n      content: \u003CContactFields form={form} \u002F>,\n      validate: hookFormValidator(form, { fields: [\"email\", \"phone\"] }),\n    },\n    {\n      id: \"address\", title: \"Address\",\n      content: \u003CAddressFields form={form} \u002F>,\n      validate: hookFormValidator(form, { fields: [\"street\", \"city\", \"postcode\"] }),\n    },\n    {\n      id: \"payment\", title: \"Payment\",\n      content: \u003CPaymentFields form={form} \u002F>,\n      validate: hookFormValidator(form, { fields: [\"cardNumber\"] }),\n    },\n  ],\n};\n\n\u003CFormWizard\n  schema={schema}\n  finishButtonText=\"Pay now\"\n  \u002F\u002F handleSubmit returns a handler, so invoke it rather than passing it through.\n  onComplete={() => void form.handleSubmit(submit)()}\n\u002F>;",[],{"id":527,"html":528,"type":124},"b104","\u003Cp>\u003Ccode>mode: &quot;onChange&quot;\u003C\u002Fcode> matters — the adapter reads \u003Ccode>formState.errors\u003C\u002Fcode>, so the errors have to be current for the gate to be accurate.\u003C\u002Fp>",{"id":530,"html":531,"text":531,"type":138,"level":43},"b105","2. Onboarding that survives a refresh",{"id":533,"code":534,"type":195,"language":196,"highlight":535},"b106","\u003CFormWizard\n  schema={schema}\n  \u002F\u002F \"session\" clears with the tab; \"local\" persists across restarts.\n  persist={{ key: \"onboarding\", storage: \"session\" }}\n  syncToUrl\n\u002F>",[],{"id":537,"html":538,"type":124},"b107","\u003Cp>That is the passport-upload bug from the opening, closed. Data goes to \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWeb_Storage_API\">web storage\u003C\u002Fa>, the cursor goes to \u003Ccode>?step=\u003C\u002Fcode>, and both are best-effort — private browsing and quota errors are caught, never thrown at the user. Clear it with \u003Ccode>ref.current.reset()\u003C\u002Fcode>.\u003C\u002Fp>",{"id":540,"html":541,"text":541,"type":138,"level":43},"b108","3. A survey that branches",{"id":543,"code":544,"type":195,"language":196,"highlight":545},"b109","steps: [\n  { id: \"role\", title: \"Your role\", content: \u003CRolePicker \u002F> },\n  {\n    id: \"team\",\n    title: \"Team size\",\n    condition: ({ data }) => data.role === \"manager\",\n    content: \u003CTeamSize \u002F>,\n  },\n  { id: \"done\", title: \"Thanks\", content: \u003CThanks \u002F> },\n];",[],{"id":547,"html":548,"type":124},"b110","\u003Cp>The rail, the progress bar, and the &quot;is this the last step&quot; logic all recount themselves. There is no index arithmetic anywhere in your code.\u003C\u002Fp>",{"id":550,"html":551,"text":551,"type":138,"level":31},"b111","When not to use it",{"id":553,"html":554,"type":124},"b112","\u003Cp>The credibility of everything above depends on this section being real.\u003C\u002Fp>",{"id":556,"type":127,"items":557,"ordered":18},"b113",[558,559,560,561,562,563],"\u003Cstrong>Your form has one step.\u003C\u002Fstrong> Then it is a form. Use \u003Ccode>&lt;form&gt;\u003C\u002Fcode> and react-hook-form, and stop reading.","\u003Cstrong>Your design system owns every pixel and you want no CSS at all.\u003C\u002Fstrong> Use \u003Ccode>@stepperize\u002Freact\u003C\u002Fcode>, or \u003Ccode>useWizard()\u003C\u002Fcode> from this package — both give you the state machine and nothing else. Picking the styled component and then fighting it with overrides is the worst of both.","\u003Cstrong>Your flow is a real state machine\u003C\u002Fstrong> — retries, async guards, server-driven transitions. Model it in XState and let the wizard be the view, or skip the wizard entirely. \u003Ccode>condition\u003C\u002Fcode> is a predicate, not a transition table.","\u003Cstrong>You need validation to run only on final submit.\u003C\u002Fstrong> The gate runs per step by design. You can pass \u003Ccode>validate: () =&gt; true\u003C\u002Fcode> everywhere and check on \u003Ccode>onComplete\u003C\u002Fcode>, but at that point the wizard is only doing layout for you.","\u003Cstrong>React 16.\u003C\u002Fstrong> Untested. It needs \u003Ccode>react\u002Fjsx-runtime\u003C\u002Fcode>, so 16.14+ in principle, but nobody has verified it.","\u003Cstrong>React 17 under native Node ESM.\u003C\u002Fstrong> It will not load — React 17 ships no \u003Ccode>exports\u003C\u002Fcode> field, so Node cannot resolve \u003Ccode>react\u002Fjsx-runtime\u003C\u002Fcode>. That is a React 17 limitation, reproducible without this package, and React 17 works fine through Vite, webpack, Next.js or CRA.",{"id":565,"html":566,"text":566,"type":138,"level":31},"b114","FAQ",{"id":568,"html":569,"text":569,"type":138,"level":43},"b115","How do I validate each step of a multi-step form in React?",{"id":571,"html":572,"type":124},"b116","\u003Cp>Give each step a pure predicate over the shared form data that returns \u003Ccode>true\u003C\u002Fcode> or an error message, and check it before advancing the cursor. Do not create one \u003Ccode>&lt;form&gt;\u003C\u002Fcode> per step — you lose cross-step values and end up reconciling four states. With this package that predicate is the \u003Ccode>validate\u003C\u002Fcode> option, and \u003Ccode>zodValidator\u003C\u002Fcode> \u002F \u003Ccode>hookFormValidator\u003C\u002Fcode> generate it from a schema or a field subset.\u003C\u002Fp>",{"id":574,"html":575,"text":575,"type":138,"level":43},"b117","How do I keep multi-step form data after a page refresh?",{"id":577,"html":578,"type":124},"b118","\u003Cp>Serialise the shared data object to \u003Ccode>sessionStorage\u003C\u002Fcode> (clears with the tab) or \u003Ccode>localStorage\u003C\u002Fcode> (survives a restart) on every change, and restore it on mount. Keep the step index in the URL rather than storage, so a shared link opens the right step. \u003Ccode>persist={{ key, storage }}\u003C\u002Fcode> and \u003Ccode>syncToUrl\u003C\u002Fcode> do both; write it yourself in about twenty lines if you prefer.\u003C\u002Fp>",{"id":580,"html":581,"text":581,"type":138,"level":43},"b119","Do multi-step forms work in the Next.js App Router?",{"id":583,"html":584,"type":124},"b120","\u003Cp>Yes, but the component holding the cursor must be a client component. This package ships a \u003Ccode>&quot;use client&quot;\u003C\u002Fcode> directive inside all three bundles, so you can import \u003Ccode>FormWizard\u003C\u002Fcode> directly from a server component without writing a wrapper. Import the stylesheet once, usually in \u003Ccode>app\u002Flayout.tsx\u003C\u002Fcode>.\u003C\u002Fp>",{"id":586,"html":587,"text":587,"type":138,"level":43},"b121","How should an embedded React component decide it is in dark mode?",{"id":589,"html":590,"type":124},"b122","\u003Cp>Read the page, not the operating system. Look for an ancestor with \u003Ccode>[data-theme=&quot;dark&quot;]\u003C\u002Fcode> or a \u003Ccode>.dark\u003C\u002Fcode> class — the conventions Tailwind, next-themes, Docusaurus and Fumadocs already set — and expose a prop for hosts that do it differently. Reading \u003Ccode>prefers-color-scheme\u003C\u002Fcode> inside a component means a light page on a dark-mode machine gets a dark widget on a white background.\u003C\u002Fp>",{"id":592,"html":593,"text":593,"type":138,"level":43},"b123","Is a step rail accessible by default?",{"id":595,"html":596,"type":124},"b124","\u003Cp>Almost never, when hand-rolled. The \u003Ca href=\"https:\u002F\u002Fwww.w3.org\u002FWAI\u002FARIA\u002Fapg\u002Fpatterns\u002Ftabs\u002F\">ARIA tabs pattern\u003C\u002Fa> wants \u003Ccode>tablist\u003C\u002Fcode>\u002F\u003Ccode>tab\u003C\u002Fcode>\u002F\u003Ccode>tabpanel\u003C\u002Fcode> roles, \u003Ccode>aria-selected\u003C\u002Fcode>, \u003Ccode>aria-controls\u003C\u002Fcode>, a roving \u003Ccode>tabIndex\u003C\u002Fcode>, arrow-key and Home\u002FEnd handling, focus moved to the revealed panel, and a live region so the change is announced at all. Related reading on the same theme: \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fthe-modal-was-open-so-was-everything-behind-it-266n\">the \u003Ccode>inert\u003C\u002Fcode> attribute\u003C\u002Fa> and \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fyou-built-your-modal-with-a-and-a-focus-trap-library-the-native-does-all-of-15lg\">what \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode> gives you for free\u003C\u002Fa>.\u003C\u002Fp>",{"id":598,"html":599,"text":599,"type":138,"level":43},"b125","Does upgrading to v2 break my styling?",{"id":601,"html":602,"type":124},"b126","\u003Cp>Only if you styled it. The component API is identical — every prop, method, hook and adapter behaves exactly as in 1.2.x. If you never wrote CSS against the wizard, you get the new look and working dark mode with no code change. If you did, \u003Ccode>variant=&quot;legacy&quot;\u003C\u002Fcode> plus the \u003Ccode>legacy.css\u003C\u002Fcode> import restores v1 pixel for pixel.\u003C\u002Fp>",{"id":604,"html":605,"text":605,"type":138,"level":31},"b127","Cheat sheet",{"id":607,"html":608,"type":124},"b128","\u003Cp>Everything you need to build a correct multi-step form, whichever library you use.\u003C\u002Fp>",{"id":610,"head":611,"rows":615,"type":255},"b129",[612,613,614],"Task","Code","Notes",[616,620,624,628,632,636,640,644,648,652,656,660,664,668,672,676,680],[617,618,619],"Static steps","\u003Ccode>&lt;FormWizard.TabContent title=&quot;…&quot;&gt;\u003C\u002Fcode>","Children API.",[621,622,623],"Data-driven steps","\u003Ccode>schema={{ steps: [...] }}\u003C\u002Fcode>","Wins over children when both are given.",[625,626,627],"Branch a step","\u003Ccode>condition: ({ data }) =&gt; …\u003C\u002Fcode>","\u003Ccode>false\u003C\u002Fcode> removes it from the rail entirely.",[629,630,631],"Gate a step","\u003Ccode>validate: ({ data }) =&gt; true | &quot;message&quot;\u003C\u002Fcode>","Pure and cheap — runs every render, must not set state.",[633,634,635],"Zod gate","\u003Ccode>zodValidator(schema, { pick: [&quot;email&quot;] })\u003C\u002Fcode>","Any \u003Ccode>safeParse\u003C\u002Fcode> object works — Zod, Valibot, ArkType.",[637,638,639],"react-hook-form gate","\u003Ccode>hookFormValidator(form, { fields: [...] })\u003C\u002Fcode>","Reads \u003Ccode>formState.errors\u003C\u002Fcode>; use \u003Ccode>mode: &quot;onChange&quot;\u003C\u002Fcode>.",[641,642,643],"Chain rules","\u003Ccode>composeValidators(a, b)\u003C\u002Fcode>","First failure wins.",[645,646,647],"Survive a refresh","\u003Ccode>persist={{ key, storage: &quot;session&quot; }}\u003C\u002Fcode>","Best-effort; quota errors never throw.",[649,650,651],"Deep link a step","\u003Ccode>syncToUrl\u003C\u002Fcode>","Writes \u003Ccode>?step=2\u003C\u002Fcode>.",[653,654,655],"Recolour","\u003Ccode>theme={{ primaryColor: &quot;#0e6f70&quot; }}\u003C\u002Fcode>","Emits \u003Ccode>--rfw-*\u003C\u002Fcode> custom properties.",[657,658,659],"Keep the v1 look","\u003Ccode>variant=&quot;legacy&quot;\u003C\u002Fcode> + \u003Ccode>legacy.css\u003C\u002Fcode>","The only v2 migration step.",[661,662,663],"Dark mode","\u003Ccode>colorScheme=&quot;auto&quot; | &quot;system&quot; | &quot;light&quot; | &quot;dark&quot;\u003C\u002Fcode>","\u003Ccode>auto\u003C\u002Fcode> follows the page; \u003Ccode>system\u003C\u002Fcode> follows the OS.",[665,666,667],"Tailwind, keep the skin","\u003Ccode>@import &quot;...\u002Ftailwind.css&quot;\u003C\u002Fcode>","v4 only — reads \u003Ccode>--color-*\u003C\u002Fcode>.",[669,670,671],"Tailwind, own the classes","\u003Ccode>unstyled classNames={tailwindPreset()}\u003C\u002Fcode>","Needs \u003Ccode>@source\u003C\u002Fcode> pointed at the package.",[673,674,675],"Style a finished step","\u003Ccode>classNames.stepComplete\u003C\u002Fcode>","Also \u003Ccode>.rfw-done\u003C\u002Fcode> \u002F \u003Ccode>.rfw-invalid\u003C\u002Fcode> in CSS.",[677,678,679],"No markup at all","\u003Ccode>useWizard({ stepIds })\u003C\u002Fcode>","Same state machine as the component.",[681,682,683],"Jump anywhere","\u003Ccode>ref.current.goToTabById(&quot;review&quot;)\u003C\u002Fcode>","Bypasses the visited-step gate.",{"id":685,"html":686,"type":124},"b130","\u003Cp>The whole pattern, annotated:\u003C\u002Fp>",{"id":688,"code":689,"type":195,"language":196,"highlight":690},"b131","import FormWizard, { zodValidator, type FormWizardSchema } from \"react-form-wizard-component\";\nimport \"react-form-wizard-component\u002Fstyles.css\";\n\nconst schema: FormWizardSchema = {\n  initialData: { plan: \"basic\" },\n  steps: [\n    { id: \"plan\", title: \"Plan\", content: \u003CPlanPicker \u002F> },\n    {\n      id: \"billing\",\n      title: \"Billing\",\n      condition: ({ data }) => data.plan === \"premium\",     \u002F\u002F cursor: branch\n      content: \u003CBillingFields \u002F>,\n      validate: zodValidator(billing, { pick: [\"card\"] }),  \u002F\u002F gate: this step only\n    },\n    { id: \"review\", title: \"Review\", content: ({ data }) => \u003CReview data={data} \u002F> },\n  ],\n};\n\n\u003CFormWizard\n  schema={schema}\n  persist={{ key: \"checkout\" }}   \u002F\u002F data: survives a refresh\n  syncToUrl                       \u002F\u002F cursor: survives a shared link\n  theme={{ primaryColor: \"#0e6f70\" }}\n  onComplete={(data) => submit(data)}\n\u002F>;",[],{"id":692,"html":693,"text":693,"type":138,"level":31},"b132","Key takeaways",{"id":695,"type":127,"items":696,"ordered":18},"b133",[697,698,699,700,701],"A multi-step form is a \u003Cstrong>cursor\u003C\u002Fstrong>, \u003Cstrong>shared data\u003C\u002Fstrong>, and a \u003Cstrong>per-step gate\u003C\u002Fstrong>. Name the three and every bug has an address.","Per-step validation means one form and a gate that picks its own fields — never four separate forms.","The cursor belongs in the URL; the data belongs in storage. Putting both in the same place is what loses the passport upload.","The step gate runs on every render. Keep it pure, keep it cheap, never set state in it.","A component reads the \u003Cem>page\u003C\u002Fem> for dark mode. Only the page itself gets to read the OS.",{"id":703,"html":704,"type":124},"b134","\u003Cp>That refresh on step four, at the top? \u003Ccode>persist\u003C\u002Fcode> puts the data back and \u003Ccode>syncToUrl\u003C\u002Fcode> puts the cursor back, and the user never learns it happened. The rest of the list — the gate, the focus, the announcement, the roving tabindex — is the part that was always going to take a week, whoever writes it.\u003C\u002Fp>",{"id":706,"html":707,"type":124},"b135","\u003Cp>What&#39;s your worst multi-step form story: the one that lost data, or the one nobody could operate with a keyboard?\u003C\u002Fp>",{"id":709,"type":120},"b136",{"id":711,"html":712,"type":124},"b137","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":714,"html":715,"type":124},"b138","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":717,"type":127,"items":718,"ordered":18},"b139",[719,720,721],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>",1,"---\ntitle: \"React Multi-Step Form: The Complete Guide (with Cheat Sheet)\"\ndescription: \"Build a React multi-step form that validates per step, survives a refresh, and stays accessible — the mental model, the traps, and a copy-paste cheat sheet.\"\ntags: [react, typescript, tutorial, webdev]\npublished: true\nseries: \"React Toolbox\"\npackage: react-form-wizard-component\nversion: 2.0.0\npreviousVersion: 1.2.1\necosystem: react\ntagline: \"Accessible multi-step form wizard for React 17, 18 and 19 — zero dependencies, styled or headless.\"\nhighlights:\n  - New default look, and dark mode that actually works\n  - Tailwind token bridge and utility preset\n  - Same API — legacy skin is one opt-in line\nlinkedin: |\n  Multi-step forms look like two useState calls until you ship one.\n\n  • A wizard is really three things wearing one component's clothes: a cursor, a shared data object, and a per-step gate. Name them and every bug has an address.\n  • Per-step validation does NOT mean four separate forms — one schema, one form, a gate that picks its own fields.\n  • An embedded component should never read prefers-color-scheme. A light page on a dark-mode laptop gets a dark widget on white.\n\n  Full write-up — the mental model, the traps, and a copy-paste cheat sheet 👇\n---\n\nThe signup form has four steps. Step three uploads a passport photo and takes eight seconds. A user reaches step four, fat-fingers ⌘R, and lands back on step one with an empty form.\n\nYou already know how to fix that. You also know that the moment the step index stops living in `useState`, the browser back button, the per-step validation, and the focus ring all become your problem too.\n\nMulti-step forms look like two `useState` calls until you ship one.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Model any React multi-step form as three separate things — a cursor, shared data, and a per-step gate — and know which one a given bug belongs to\n- Validate one step's fields without splitting your form into four forms\n- Keep wizard state across a refresh and a shared link, without a router\n- Explain why an embedded component should ignore `prefers-color-scheme`\n- Ship the whole thing with correct ARIA roles, focus movement, and keyboard support\n\n## Who this is for\n\nYou've built a React form with controlled inputs and reached for a validation library at least once. TypeScript is used throughout but nothing here depends on it.\n\n## Table of contents\n\n- [Why multi-step forms are harder than they look](#why-multi-step-forms-are-harder-than-they-look)\n- [What people build today](#what-people-build-today)\n- [Where the usual options actually break](#where-the-usual-options-actually-break)\n- [The mental model](#the-mental-model)\n- [Enter react-form-wizard-component](#enter-react-form-wizard-component)\n- [The API, in four stages](#the-api-in-four-stages)\n- [What's new in v2.0](#whats-new-in-v20)\n- [Three recipes](#three-recipes)\n- [When not to use it](#when-not-to-use-it)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n\n## Why multi-step forms are harder than they look\n\nHere is the version everyone writes first, in full:\n\n```tsx\nfunction Signup() {\n  const [step, setStep] = useState(0);\n  const [data, setData] = useState({});\n\n  return (\n    \u003C>\n      {step === 0 && \u003CAccount data={data} onChange={setData} \u002F>}\n      {step === 1 && \u003CProfile data={data} onChange={setData} \u002F>}\n      {step === 2 && \u003CReview data={data} \u002F>}\n\n      \u003Cbutton onClick={() => setStep((s) => s - 1)} disabled={step === 0}>Back\u003C\u002Fbutton>\n      \u003Cbutton onClick={() => setStep((s) => s + 1)}>Next\u003C\u002Fbutton>\n    \u003C\u002F>\n  );\n}\n```\n\nIt works. It also ships five bugs, and each one costs about a day:\n\n1. **Next always advances.** There is no gate. A user tabs past every required field and lands on Review with an empty object.\n2. **A refresh is a total loss.** `step` and `data` are component state; ⌘R resets both.\n3. **The browser back button leaves the page.** Users expect Back to mean \"previous step.\" Yours means \"goodbye.\"\n4. **A screen reader is told nothing.** The panel swaps, the DOM changes, and the announcement queue stays empty. Focus is still sitting on the Next button that just disappeared and got re-rendered as a different button.\n5. **You cannot link to step 3.** Support asks a customer to \"go to the payment step\" and has to describe the route in words.\n\nNone of those are hard *individually*. The problem is that they are five different subsystems, and the naive version has no seam to hang any of them on.\n\n## What people build today\n\nThe honest survey, with versions checked against npm at the time of writing:\n\n| Package | Latest | Last published | Shape |\n|---|---|---|---|\n| [`react-hook-form`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-hook-form) | 7.87.0 | Aug 2026 | Form state. No opinion about steps at all. |\n| [`formik`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fformik) | 2.4.9 | Nov 2025 | Same — form state, no cursor. |\n| [`@stepperize\u002Freact`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@stepperize\u002Freact) | 7.0.0 | Jun 2026 | Headless, type-safe, actively maintained. Ships zero UI by design. |\n| [`react-step-wizard`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-step-wizard) | 5.3.11 | Aug 2024 | Styled component, quiet for two years. |\n| [`react-multistep`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-multistep) | 7.0.0 | Feb 2026 | Styled component, opinionated markup. |\n| [`react-albus`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-albus) | 2.0.0 | Jun 2022 | Router-flavoured wizard. Three runtime deps. |\n\nTwo of these deserve a genuine defence.\n\n**react-hook-form is the right answer to a different question.** It is the best form-state library in the React ecosystem and it is not trying to be a wizard. Nothing below argues against using it — the recipes actively use it.\n\n**@stepperize\u002Freact is good, current, and well typed.** If your design system already owns every pixel and you want nothing but a state machine, use it. It ships no markup on purpose, which means you write the rail, the progress bar, the buttons, and all of the [ARIA tabs wiring](https:\u002F\u002Fwww.w3.org\u002FWAI\u002FARIA\u002Fapg\u002Fpatterns\u002Ftabs\u002F) yourself. That is a feature when you have a design system, and a week of work when you don't.\n\n## Where the usual options actually break\n\nThree concrete gaps, not vibes.\n\n**The peer range that stops the install.** `react-multistep@7.0.0` declares `peerDependencies` as `{ \"react\": \"18.3.1\" }` — an exact version, not a range. On a React 19 app, npm refuses:\n\n```\nnpm error code ERESOLVE\nnpm error ERESOLVE unable to resolve dependency tree\nnpm error Found: react@19.2.0\nnpm error Could not resolve dependency:\nnpm error peer react@\"18.3.1\" from react-multistep@7.0.0\nnpm error Fix the upstream dependency conflict, or retry\nnpm error this command with --force or --legacy-peer-deps\n```\n\nYou can `--legacy-peer-deps` past it. You are then running a component whose author has not tested your React version.\n\n**Headless means you own the accessibility.** Here is the minimum the tabs pattern asks for, and none of it comes free:\n\n```tsx\n\u003Cdiv role=\"tablist\" aria-label=\"Signup steps\">\n  \u003Cbutton\n    role=\"tab\"\n    id=\"tab-account\"\n    aria-selected={step === 0}\n    aria-controls=\"panel-account\"\n    tabIndex={step === 0 ? 0 : -1}   \u002F\u002F roving tabindex, not 0 on every tab\n  >\n    Account\n  \u003C\u002Fbutton>\n  {\u002F* …and arrow-key handling, and Home\u002FEnd, and focus movement on change, *\u002F}\n  {\u002F* and an aria-live region so the change is announced at all *\u002F}\n\u003C\u002Fdiv>\n```\n\nThat is a real day of work, and it is the part that gets cut when the sprint is tight.\n\n**Styled-but-stale means you inherit the CSS.** `react-albus` was last published in June 2022 and still pulls in three runtime dependencies (`history`, `hoist-non-react-statics`, `prop-types`). A styled component that has stopped shipping is one you restyle with `!important`, which is the sound of a dependency that has stopped helping.\n\n## The mental model\n\nThis is the section worth keeping even if you install nothing.\n\n**The mental model:** a multi-step form is three independent things wearing one component's clothes.\n\n1. **A cursor** — which step is showing, plus the furthest step reached. Two numbers.\n2. **Shared data** — one object every step reads and writes. Not one state per step.\n3. **A gate per step** — a pure function answering one question: *may the user leave this step?* It returns `true`, or a string that is both \"no\" and the reason.\n\nEverything else is a *projection* of those three. The progress bar is a projection of the cursor. Persistence is serialising the data. A deep link is the cursor in the URL. Focus management is a side effect of the cursor changing.\n\nThe payoff is diagnostic. Every wizard bug now has an address:\n\n- A lost draft is a **data** problem.\n- A Next button that won't move is a **gate** problem.\n- A back button that exits the page is a **cursor** problem.\n- Steps that unlock when they shouldn't is a **cursor** problem too — specifically the \"furthest reached\" number.\n\nNote what the gate is *not*: it is not a submit handler. It runs on every render, so it must be pure and cheap, and it must never set state. Get that wrong and you have an infinite render loop rather than a validation bug.\n\n## Enter react-form-wizard-component\n\nThe three pieces above are exactly what [`react-form-wizard-component`](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-form-wizard-component) implements, and **v2.0.0** landed on npm at the end of August 2026.\n\n```bash\nnpm install react-form-wizard-component\n```\n\n```tsx\nimport FormWizard from \"react-form-wizard-component\";\nimport \"react-form-wizard-component\u002Fstyles.css\";\n\nexport default function Signup() {\n  return (\n    \u003CFormWizard onComplete={(data) => console.log(data)}>\n      \u003CFormWizard.TabContent title=\"Account\">\n        \u003Cinput placeholder=\"Email\" \u002F>\n      \u003C\u002FFormWizard.TabContent>\n      \u003CFormWizard.TabContent title=\"Profile\">\n        \u003Cinput placeholder=\"Full name\" \u002F>\n      \u003C\u002FFormWizard.TabContent>\n      \u003CFormWizard.TabContent title=\"Review\">\n        \u003Cp>Looks good?\u003C\u002Fp>\n      \u003C\u002FFormWizard.TabContent>\n    \u003C\u002FFormWizard>\n  );\n}\n```\n\nThat is a working wizard with a progress bar, arrow-key and Home\u002FEnd navigation, swipe on touch, focus movement, and live-region announcements.\n\nHonestly, in one sentence: **a styled component and a headless hook from the same package — 5.85 kB minified and brotlied, zero runtime dependencies, React 17, 18 and 19.** Those size numbers are the ones CI measures with `size-limit` on every commit, not a bundlephobia guess; the stylesheet is another 1.38 kB.\n\n- 📦 npm: \u003Chttps:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-form-wizard-component>\n- 🐙 GitHub: \u003Chttps:\u002F\u002Fgithub.com\u002Fparsajiravand\u002Freact-form-wizard>\n- 📚 Docs: \u003Chttps:\u002F\u002Freact-form-wizard-component-document.netlify.app\u002Fdocs>\n- ▶️ Live demos: \u003Chttps:\u002F\u002Freact-form-wizard-component-document.netlify.app\u002Fdocs\u002Fdemos> · [interactive playground](https:\u002F\u002Freact-form-wizard-component-document.netlify.app\u002Fdocs\u002Fplayground)\n\n## The API, in four stages\n\n### Stage 1 — children, when the steps are static\n\nThe example above. Each `\u003CFormWizard.TabContent>` is a step; `title` and `icon` are optional.\n\n**Key concept:** the children API owns the cursor for you and nothing else. It is the right choice only while every user sees every step.\n\n### Stage 2 — schema, when steps depend on data\n\n```tsx\nimport FormWizard, { type FormWizardSchema } from \"react-form-wizard-component\";\n\nconst schema: FormWizardSchema = {\n  initialData: { plan: \"basic\" },\n  steps: [\n    { id: \"plan\", title: \"Plan\", content: \u003CPlanPicker \u002F> },\n    {\n      id: \"billing\",\n      title: \"Billing\",\n      \u002F\u002F The step disappears from the rail entirely when this is false.\n      condition: ({ data }) => data.plan === \"premium\",\n      content: \u003CBillingFields \u002F>,\n    },\n    {\n      id: \"review\",\n      title: \"Review\",\n      \u002F\u002F content can be a function of the current data.\n      content: ({ data }) => \u003CReview data={data} \u002F>,\n    },\n  ],\n};\n\n\u003CFormWizard schema={schema} onComplete={(data) => submit(data)} \u002F>;\n```\n\n**Key concept:** `condition` is evaluated against shared data, so branching is declarative. You never write `steps.filter(...)` and then fix the off-by-one in the progress bar.\n\n### Stage 3 — the gate, and the adapters that write it for you\n\nThe contract is deliberately tiny. Return `true` to allow navigation, a **string** to block it and supply the message, `false` to block silently:\n\n```tsx\nvalidate: ({ data }) => (data.accepted ? true : \"Please accept the terms\"),\n```\n\nTwo adapters produce that function from tools you already use. With Zod:\n\n```tsx\nimport { z } from \"zod\";\nimport FormWizard, { zodValidator } from \"react-form-wizard-component\";\n\nconst account = z.object({\n  email: z.string().email(\"Enter a valid email\"),\n  password: z.string().min(8, \"At least 8 characters\"),\n});\n\n\u002F\u002F `pick` narrows the whole wizard data down to this step's slice.\nvalidate: zodValidator(account, { pick: [\"email\", \"password\"] });\n```\n\nWith react-hook-form, gating a step on a subset of one form's fields:\n\n```tsx\nimport { useForm } from \"react-hook-form\";\nimport FormWizard, { hookFormValidator } from \"react-form-wizard-component\";\n\nconst form = useForm({ mode: \"onChange\" });\n\nvalidate: hookFormValidator(form, { fields: [\"email\", \"phone\"] });\n```\n\n**Key concept:** this is what \"per-step validation\" should mean — *one* form, one schema, and a gate that only looks at its own fields. Not four forms glued together with a reducer.\n\nNeither `zod` nor `react-hook-form` becomes a dependency. Both adapters are typed structurally: `zodValidator` accepts anything with a `safeParse` method, so Valibot and ArkType work too, and `hookFormValidator` reads `formState.errors` and nothing else — it never mutates form state, which is what keeps it safe to run on every render. Combine rules with `composeValidators(...)`; first failure wins.\n\n### Stage 4 — headless, when you own the markup\n\n```tsx\nimport { useWizard } from \"react-form-wizard-component\";\n\nconst wizard = useWizard({\n  stepIds: [\"account\", \"profile\", \"review\"],\n  persist: { key: \"signup\" },\n  syncToUrl: true, \u002F\u002F writes ?step=2\n});\n\nwizard.currentStep;    \u002F\u002F 0-based cursor\nwizard.maxVisitedStep; \u002F\u002F the forward gate\nwizard.data;           \u002F\u002F shared data\nwizard.next();\nwizard.goToId(\"review\");\nwizard.updateData({ email }); \u002F\u002F merges\n```\n\n**Key concept:** `\u003CFormWizard \u002F>` is built on this hook. Choosing headless is not choosing a different library — the state machine is identical, you are only declining the markup and the stylesheet.\n\n## What's new in v2.0\n\nHere is the part worth knowing if you are already on 1.x.\n\n**Every prop, method, hook and adapter is unchanged.** v2.0.0 is a major version because the *default appearance* changed, and a visual change to a component you already styled is a breaking change even when the types say otherwise. Semver's job is to warn you before the deploy, not after.\n\nKeeping the exact v1 look is a two-line diff:\n\n```diff\n  import \"react-form-wizard-component\u002Fstyles.css\";\n+ import \"react-form-wizard-component\u002Flegacy.css\";\n\n- \u003CFormWizard … \u002F>\n+ \u003CFormWizard variant=\"legacy\" … \u002F>\n```\n\n### Dark mode that follows the page, not the machine\n\nThis is the most interesting decision in the release, and it generalises well beyond this package.\n\nv1's stylesheet had no `prefers-color-scheme` rules at all — dark mode existed only as inline styles you hand-listed through a `customDarkModeColor` prop. The obvious fix is to add a [`prefers-color-scheme`](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@media\u002Fprefers-color-scheme) block. That fix is wrong, and here is why:\n\n> **An embedded component cannot assume the OS preference describes the surface behind it.** A light-only marketing page, viewed on a laptop set to dark, would render a dark wizard on a white card.\n\nSo the default (`colorScheme=\"auto\"`) reacts only to explicit signals from the host page: an ancestor carrying `[data-theme=\"dark\"]` or `.dark` — which is what Tailwind, next-themes, Docusaurus and Fumadocs all set — or the `darkMode` prop.\n\n```tsx\n\u003CFormWizard \u002F>                        {\u002F* auto: follows the page *\u002F}\n\u003CFormWizard colorScheme=\"system\" \u002F>   {\u002F* opt in to prefers-color-scheme *\u002F}\n\u003CFormWizard colorScheme=\"dark\" \u002F>     {\u002F* pin it *\u002F}\n```\n\nIf you own the whole page rather than a component inside someone else's, the media query is the right tool — I wrote about the [`light-dark()` CSS function](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fyoure-writing-dark-mode-colors-twice-light-dark-fixes-that-44hb) for exactly that case. The rule of thumb: **the page may read the OS; a component should read the page.**\n\n### The skin stopped painting inline styles\n\nv1 set colours as inline styles on the markers, the rail and the footer. Inline styles beat every selector you can write short of `!important`, which is why theming and dark mode were unreliable. In v2, all of it flows from CSS custom properties and state classes:\n\n```css\n.react-form-wizard {\n  --rfw-primary: #0e6f70;\n  --rfw-radius: 8px;\n}\n```\n\nYour CSS wins now, with normal specificity. Each step also carries state classes — `active`, `rfw-done`, `rfw-invalid` — so you can style a completed or failed step directly.\n\nAnd because the v1 skin moved into its own opt-in stylesheet, the default is *smaller* than 1.2.1 despite gaining a full dark palette: **1.38 kB** brotlied, down from 1.85 kB.\n\n### Steps no longer turn red before the user does anything\n\nA genuine bug, and my favourite kind — the one that only shows up in a real app. `showErrorOnTab` defaulted to `!isValid`, so any step carrying a validator painted its marker red on first paint. A wizard whose first question was required looked broken on load: the user is accused before they have typed a character.\n\nThe error state now waits until the user has actually tried to leave the step:\n\n```tsx\n\u002F\u002F Internals, for the curious — a Set of step ids, added to on a blocked next().\nshowErrorOnTab={step.showErrorOnTab ?? (attemptedSteps.has(step.id) && !step.isValid)}\n```\n\nAn explicit `showErrorOnTab` still applies immediately.\n\n### Tailwind, two ways\n\nKeep the bundled skin but adopt your theme — one import, no `unstyled`, Tailwind v4:\n\n```css\n@import \"tailwindcss\";\n@import \"react-form-wizard-component\u002Fstyles.css\";\n@import \"react-form-wizard-component\u002Ftailwind.css\";\n```\n\nOr build it entirely from utility classes:\n\n```tsx\nimport FormWizard, { tailwindPreset } from \"react-form-wizard-component\";\n\n\u003CFormWizard unstyled classNames={tailwindPreset()} schema={schema} \u002F>;\n```\n\nOne trap worth naming: the preset's classes live in `node_modules`, and Tailwind only emits classes it can *see*. Point it at the package or you will get an unstyled wizard and no error message:\n\n```css\n\u002F* Tailwind v4 *\u002F\n@source \"..\u002Fnode_modules\u002Freact-form-wizard-component\u002Fdist\u002F**\u002F*.js\";\n```\n\nEvery class in the preset is a literal string for that reason — nothing is built by template concatenation, because `` `bg-${colour}-600` `` is invisible to a scanner. The accent reads from `bg-[var(--rfw-primary)]`, so recolouring stays a one-line CSS change instead of a rebuild.\n\n## Three recipes\n\n### 1. Checkout — one form, three gates\n\n```tsx\nconst form = useForm\u003CCheckout>({ resolver: zodResolver(checkoutSchema), mode: \"onChange\" });\n\nconst schema: FormWizardSchema = {\n  steps: [\n    {\n      id: \"contact\", title: \"Contact\",\n      content: \u003CContactFields form={form} \u002F>,\n      validate: hookFormValidator(form, { fields: [\"email\", \"phone\"] }),\n    },\n    {\n      id: \"address\", title: \"Address\",\n      content: \u003CAddressFields form={form} \u002F>,\n      validate: hookFormValidator(form, { fields: [\"street\", \"city\", \"postcode\"] }),\n    },\n    {\n      id: \"payment\", title: \"Payment\",\n      content: \u003CPaymentFields form={form} \u002F>,\n      validate: hookFormValidator(form, { fields: [\"cardNumber\"] }),\n    },\n  ],\n};\n\n\u003CFormWizard\n  schema={schema}\n  finishButtonText=\"Pay now\"\n  \u002F\u002F handleSubmit returns a handler, so invoke it rather than passing it through.\n  onComplete={() => void form.handleSubmit(submit)()}\n\u002F>;\n```\n\n`mode: \"onChange\"` matters — the adapter reads `formState.errors`, so the errors have to be current for the gate to be accurate.\n\n### 2. Onboarding that survives a refresh\n\n```tsx\n\u003CFormWizard\n  schema={schema}\n  \u002F\u002F \"session\" clears with the tab; \"local\" persists across restarts.\n  persist={{ key: \"onboarding\", storage: \"session\" }}\n  syncToUrl\n\u002F>\n```\n\nThat is the passport-upload bug from the opening, closed. Data goes to [web storage](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWeb_Storage_API), the cursor goes to `?step=`, and both are best-effort — private browsing and quota errors are caught, never thrown at the user. Clear it with `ref.current.reset()`.\n\n### 3. A survey that branches\n\n```tsx\nsteps: [\n  { id: \"role\", title: \"Your role\", content: \u003CRolePicker \u002F> },\n  {\n    id: \"team\",\n    title: \"Team size\",\n    condition: ({ data }) => data.role === \"manager\",\n    content: \u003CTeamSize \u002F>,\n  },\n  { id: \"done\", title: \"Thanks\", content: \u003CThanks \u002F> },\n];\n```\n\nThe rail, the progress bar, and the \"is this the last step\" logic all recount themselves. There is no index arithmetic anywhere in your code.\n\n## When not to use it\n\nThe credibility of everything above depends on this section being real.\n\n- **Your form has one step.** Then it is a form. Use `\u003Cform>` and react-hook-form, and stop reading.\n- **Your design system owns every pixel and you want no CSS at all.** Use `@stepperize\u002Freact`, or `useWizard()` from this package — both give you the state machine and nothing else. Picking the styled component and then fighting it with overrides is the worst of both.\n- **Your flow is a real state machine** — retries, async guards, server-driven transitions. Model it in XState and let the wizard be the view, or skip the wizard entirely. `condition` is a predicate, not a transition table.\n- **You need validation to run only on final submit.** The gate runs per step by design. You can pass `validate: () => true` everywhere and check on `onComplete`, but at that point the wizard is only doing layout for you.\n- **React 16.** Untested. It needs `react\u002Fjsx-runtime`, so 16.14+ in principle, but nobody has verified it.\n- **React 17 under native Node ESM.** It will not load — React 17 ships no `exports` field, so Node cannot resolve `react\u002Fjsx-runtime`. That is a React 17 limitation, reproducible without this package, and React 17 works fine through Vite, webpack, Next.js or CRA.\n\n## FAQ\n\n### How do I validate each step of a multi-step form in React?\n\nGive each step a pure predicate over the shared form data that returns `true` or an error message, and check it before advancing the cursor. Do not create one `\u003Cform>` per step — you lose cross-step values and end up reconciling four states. With this package that predicate is the `validate` option, and `zodValidator` \u002F `hookFormValidator` generate it from a schema or a field subset.\n\n### How do I keep multi-step form data after a page refresh?\n\nSerialise the shared data object to `sessionStorage` (clears with the tab) or `localStorage` (survives a restart) on every change, and restore it on mount. Keep the step index in the URL rather than storage, so a shared link opens the right step. `persist={{ key, storage }}` and `syncToUrl` do both; write it yourself in about twenty lines if you prefer.\n\n### Do multi-step forms work in the Next.js App Router?\n\nYes, but the component holding the cursor must be a client component. This package ships a `\"use client\"` directive inside all three bundles, so you can import `FormWizard` directly from a server component without writing a wrapper. Import the stylesheet once, usually in `app\u002Flayout.tsx`.\n\n### How should an embedded React component decide it is in dark mode?\n\nRead the page, not the operating system. Look for an ancestor with `[data-theme=\"dark\"]` or a `.dark` class — the conventions Tailwind, next-themes, Docusaurus and Fumadocs already set — and expose a prop for hosts that do it differently. Reading `prefers-color-scheme` inside a component means a light page on a dark-mode machine gets a dark widget on a white background.\n\n### Is a step rail accessible by default?\n\nAlmost never, when hand-rolled. The [ARIA tabs pattern](https:\u002F\u002Fwww.w3.org\u002FWAI\u002FARIA\u002Fapg\u002Fpatterns\u002Ftabs\u002F) wants `tablist`\u002F`tab`\u002F`tabpanel` roles, `aria-selected`, `aria-controls`, a roving `tabIndex`, arrow-key and Home\u002FEnd handling, focus moved to the revealed panel, and a live region so the change is announced at all. Related reading on the same theme: [the `inert` attribute](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fthe-modal-was-open-so-was-everything-behind-it-266n) and [what `\u003Cdialog>` gives you for free](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fyou-built-your-modal-with-a-and-a-focus-trap-library-the-native-does-all-of-15lg).\n\n### Does upgrading to v2 break my styling?\n\nOnly if you styled it. The component API is identical — every prop, method, hook and adapter behaves exactly as in 1.2.x. If you never wrote CSS against the wizard, you get the new look and working dark mode with no code change. If you did, `variant=\"legacy\"` plus the `legacy.css` import restores v1 pixel for pixel.\n\n## Cheat sheet\n\nEverything you need to build a correct multi-step form, whichever library you use.\n\n| Task | Code | Notes |\n|---|---|---|\n| Static steps | `\u003CFormWizard.TabContent title=\"…\">` | Children API. |\n| Data-driven steps | `schema={{ steps: [...] }}` | Wins over children when both are given. |\n| Branch a step | `condition: ({ data }) => …` | `false` removes it from the rail entirely. |\n| Gate a step | `validate: ({ data }) => true \\| \"message\"` | Pure and cheap — runs every render, must not set state. |\n| Zod gate | `zodValidator(schema, { pick: [\"email\"] })` | Any `safeParse` object works — Zod, Valibot, ArkType. |\n| react-hook-form gate | `hookFormValidator(form, { fields: [...] })` | Reads `formState.errors`; use `mode: \"onChange\"`. |\n| Chain rules | `composeValidators(a, b)` | First failure wins. |\n| Survive a refresh | `persist={{ key, storage: \"session\" }}` | Best-effort; quota errors never throw. |\n| Deep link a step | `syncToUrl` | Writes `?step=2`. |\n| Recolour | `theme={{ primaryColor: \"#0e6f70\" }}` | Emits `--rfw-*` custom properties. |\n| Keep the v1 look | `variant=\"legacy\"` + `legacy.css` | The only v2 migration step. |\n| Dark mode | `colorScheme=\"auto\" \\| \"system\" \\| \"light\" \\| \"dark\"` | `auto` follows the page; `system` follows the OS. |\n| Tailwind, keep the skin | `@import \"...\u002Ftailwind.css\"` | v4 only — reads `--color-*`. |\n| Tailwind, own the classes | `unstyled classNames={tailwindPreset()}` | Needs `@source` pointed at the package. |\n| Style a finished step | `classNames.stepComplete` | Also `.rfw-done` \u002F `.rfw-invalid` in CSS. |\n| No markup at all | `useWizard({ stepIds })` | Same state machine as the component. |\n| Jump anywhere | `ref.current.goToTabById(\"review\")` | Bypasses the visited-step gate. |\n\nThe whole pattern, annotated:\n\n```tsx\nimport FormWizard, { zodValidator, type FormWizardSchema } from \"react-form-wizard-component\";\nimport \"react-form-wizard-component\u002Fstyles.css\";\n\nconst schema: FormWizardSchema = {\n  initialData: { plan: \"basic\" },\n  steps: [\n    { id: \"plan\", title: \"Plan\", content: \u003CPlanPicker \u002F> },\n    {\n      id: \"billing\",\n      title: \"Billing\",\n      condition: ({ data }) => data.plan === \"premium\",     \u002F\u002F cursor: branch\n      content: \u003CBillingFields \u002F>,\n      validate: zodValidator(billing, { pick: [\"card\"] }),  \u002F\u002F gate: this step only\n    },\n    { id: \"review\", title: \"Review\", content: ({ data }) => \u003CReview data={data} \u002F> },\n  ],\n};\n\n\u003CFormWizard\n  schema={schema}\n  persist={{ key: \"checkout\" }}   \u002F\u002F data: survives a refresh\n  syncToUrl                       \u002F\u002F cursor: survives a shared link\n  theme={{ primaryColor: \"#0e6f70\" }}\n  onComplete={(data) => submit(data)}\n\u002F>;\n```\n\n## Key takeaways\n\n- A multi-step form is a **cursor**, **shared data**, and a **per-step gate**. Name the three and every bug has an address.\n- Per-step validation means one form and a gate that picks its own fields — never four separate forms.\n- The cursor belongs in the URL; the data belongs in storage. Putting both in the same place is what loses the passport upload.\n- The step gate runs on every render. Keep it pure, keep it cheap, never set state in it.\n- A component reads the *page* for dark mode. Only the page itself gets to read the OS.\n\nThat refresh on step four, at the top? `persist` puts the data back and `syncToUrl` puts the cursor back, and the user never learns it happened. The rest of the list — the gate, the focus, the announcement, the roving tabindex — is the part that was always going to take a week, whoever writes it.\n\nWhat's your worst multi-step form story: the one that lost data, or the one nobody could operate with a keyboard?\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)\n",{"title":95,"description":97},"01a05242-45d4-724a-b453-6c8023bfd7f7",{"id":727,"locked":18},"01a05242-463b-76bc-b466-a830c60c2e1e",[],[730],{"locale":13,"slug":94},{"items":732,"meta":855},[733,752,762,782,800,819,837],{"id":734,"slug":735,"title":736,"subtitle":105,"excerpt":737,"coverUrl":738,"locale":13,"readingMinutes":80,"publishedAt":739,"viewCount":740,"likeCount":19,"commentCount":19,"author":741,"vertical":742,"topic":743,"tags":745,"_count":750,"playground":751,"hasQuiz":17,"hasPlayground":17},"01a037b3-f71a-755b-9820-9f2489daaebd","css-aspect-ratio-replace-padding-hack","Delete Your padding-bottom Aspect-Ratio Hack","For a decade the only way to keep a responsive box's shape was a padding percentage and an absolutely positioned wrapper. One CSS property replaces it — with one gotcha on real images nobody warns you about.","\u002Fmedia\u002Fcovers\u002Fcss-aspect-ratio-replace-padding-hack.png","2026-08-30T11:36:40.014Z",34,{"id":102,"name":103,"username":104,"avatarUrl":105,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":744},"CSS",[746,747,748,749],{"slug":58,"name":59,"color":105},{"slug":46,"name":47,"color":105},{"slug":50,"name":51,"color":105},{"slug":62,"name":63,"color":105},{"assessments":722},{"slug":735},{"id":93,"slug":94,"title":95,"subtitle":96,"excerpt":97,"coverUrl":98,"locale":13,"readingMinutes":99,"publishedAt":100,"viewCount":753,"likeCount":19,"commentCount":19,"author":754,"vertical":755,"topic":105,"tags":756,"_count":761,"playground":105,"hasQuiz":18,"hasPlayground":18},62,{"id":102,"name":103,"username":104,"avatarUrl":105,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},[757,758,759,760],{"slug":50,"name":51,"color":105},{"slug":74,"name":75,"color":105},{"slug":112,"name":112,"color":105},{"slug":114,"name":114,"color":105},{"assessments":19},{"id":763,"slug":764,"title":765,"subtitle":105,"excerpt":766,"coverUrl":767,"locale":13,"readingMinutes":88,"publishedAt":768,"viewCount":769,"likeCount":19,"commentCount":19,"author":770,"vertical":771,"topic":772,"tags":773,"_count":780,"playground":781,"hasQuiz":17,"hasPlayground":17},"01a03297-a2d3-7318-bffe-5edb58824c5d","css-focus-visible-keyboard-focus-rings","The outline: none Debate Has Been Over Since 2020","A designer flags the ugly blue focus ring in review. An engineer deletes it with outline: none. Nobody notices the keyboard-only user who now can't tell where they are on the page.","\u002Fmedia\u002Fcovers\u002Fcss-focus-visible-keyboard-focus-rings.png","2026-08-29T12:32:09.792Z",97,{"id":102,"name":103,"username":104,"avatarUrl":105,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":744},[774,775,778,779],{"slug":58,"name":59,"color":105},{"slug":776,"name":777,"color":105},"accessibility","Accessibility",{"slug":46,"name":47,"color":105},{"slug":50,"name":51,"color":105},{"assessments":722},{"slug":764},{"id":783,"slug":784,"title":785,"subtitle":105,"excerpt":786,"coverUrl":787,"locale":13,"readingMinutes":84,"publishedAt":788,"viewCount":789,"likeCount":19,"commentCount":19,"author":790,"vertical":791,"topic":792,"tags":793,"_count":798,"playground":799,"hasQuiz":17,"hasPlayground":17},"01a02d61-19cf-72cd-8490-a59e14f8839c","css-grid-auto-fit-vs-auto-fill","Your Grid Has Invisible Columns: auto-fill vs auto-fit","A responsive card grid that works perfectly with twelve items breaks with two — not from a bug, but from one word in a repeat() you copy-pasted without reading.","\u002Fmedia\u002Fcovers\u002Fcss-grid-auto-fit-vs-auto-fill.png","2026-08-28T17:00:06.666Z",102,{"id":102,"name":103,"username":104,"avatarUrl":105,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":744},[794,795,796,797],{"slug":58,"name":59,"color":105},{"slug":46,"name":47,"color":105},{"slug":50,"name":51,"color":105},{"slug":62,"name":63,"color":105},{"assessments":722},{"slug":784},{"id":801,"slug":802,"title":803,"subtitle":105,"excerpt":804,"coverUrl":805,"locale":13,"readingMinutes":84,"publishedAt":806,"viewCount":807,"likeCount":19,"commentCount":19,"author":808,"vertical":809,"topic":810,"tags":812,"_count":817,"playground":818,"hasQuiz":17,"hasPlayground":17},"01a02839-480f-7428-914a-2c0442d00855","intl-segmenter-grapheme-word-sentence","You're truncating bios with `.slice()`. `Intl.Segmenter` knows where the emoji actually end.","A 30-character bio limit that cuts off mid-emoji isn't a rendering bug — it's .length counting UTF-16 code units instead of what's on screen. Intl.Segmenter counts graphemes, words, and sentences the way a reader actually sees them, and every major browser supports it now.","\u002Fmedia\u002Fcovers\u002Fintl-segmenter-grapheme-word-sentence.png","2026-08-27T17:30:57.707Z",173,{"id":102,"name":103,"username":104,"avatarUrl":105,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":54,"name":811},"JavaScript",[813,814,815,816],{"slug":54,"name":55,"color":105},{"slug":46,"name":47,"color":105},{"slug":50,"name":51,"color":105},{"slug":62,"name":63,"color":105},{"assessments":722},{"slug":802},{"id":820,"slug":821,"title":822,"subtitle":105,"excerpt":823,"coverUrl":824,"locale":13,"readingMinutes":84,"publishedAt":825,"viewCount":826,"likeCount":19,"commentCount":19,"author":827,"vertical":828,"topic":829,"tags":830,"_count":835,"playground":836,"hasQuiz":17,"hasPlayground":17},"01a02319-4485-7310-b494-dc94bdbfb49c","css-field-sizing-auto-grow-textarea","Delete Your Auto-Resize Textarea JS. CSS Does It Now.","The scrollHeight hack everyone copy-pastes for auto-growing textareas has a one-line CSS replacement — plus the one gotcha that bites if you skip max-height.","\u002Fmedia\u002Fcovers\u002Fcss-field-sizing-auto-grow-textarea.png","2026-08-26T06:56:38.974Z",198,{"id":102,"name":103,"username":104,"avatarUrl":105,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":58,"name":744},[831,832,833,834],{"slug":58,"name":59,"color":105},{"slug":46,"name":47,"color":105},{"slug":50,"name":51,"color":105},{"slug":54,"name":55,"color":105},{"assessments":722},{"slug":821},{"id":838,"slug":839,"title":840,"subtitle":105,"excerpt":841,"coverUrl":842,"locale":13,"readingMinutes":84,"publishedAt":843,"viewCount":844,"likeCount":19,"commentCount":19,"author":845,"vertical":846,"topic":847,"tags":848,"_count":853,"playground":854,"hasQuiz":17,"hasPlayground":17},"01a01df1-d6d5-71e8-bba6-1242e581730b","web-animations-api-imperative-control","CSS Animations Can't Pause. The Web Animations API Can.","Toggling a class to restart a CSS animation works — until you need to pause it, reverse it mid-flight, or ask how far through it is. element.animate() returns an object with those methods built in, natively, in every browser you already support.","\u002Fmedia\u002Fcovers\u002Fweb-animations-api-imperative-control.png","2026-08-25T06:55:40.108Z",189,{"id":102,"name":103,"username":104,"avatarUrl":105,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":54,"name":811},[849,850,851,852],{"slug":54,"name":55,"color":105},{"slug":58,"name":59,"color":105},{"slug":46,"name":47,"color":105},{"slug":50,"name":51,"color":105},{"assessments":722},{"slug":839},{"page":722,"perPage":76,"total":52,"totalPages":856},10,"\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Signup\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>step\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> setStep\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useState\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> setData\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useState\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>step \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">Account\u003C\u002Fspan> \u003Cspan class=\"shj-class\">data\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onChange\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>setData\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n      \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>step \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">Profile\u003C\u002Fspan> \u003Cspan class=\"shj-class\">data\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onChange\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>setData\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n      \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>step \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-num\">2\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">Review\u003C\u002Fspan> \u003Cspan class=\"shj-class\">data\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onClick\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">setStep\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>s\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> s \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-class\">disabled\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>step \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>Back\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onClick\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">setStep\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>s\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> s \u003Cspan class=\"shj-oper\">+\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>Next\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-plain shj-multiline\" data-lang=\"plain\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">npm error code ERESOLVE\nnpm error ERESOLVE unable to resolve dependency tree\nnpm error Found: react@19.2.0\nnpm error Could not resolve dependency:\nnpm error peer react@\u003Cspan class=\"shj-str\">\"18.3.1\"\u003C\u002Fspan> from react-multistep@7.0.0\nnpm error Fix the upstream dependency conflict, or retry\nnpm error this command with --force or --legacy-peer-deps\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">div\u003C\u002Fspan> \u003Cspan class=\"shj-class\">role\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"tablist\"\u003C\u002Fspan> \u003Cspan class=\"shj-class\">aria-label\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"Signup steps\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  &lt;button\n    role=\"tab\"\n    id=\"tab-account\"\n    aria-selected=\u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>step \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n    aria-controls=\"panel-account\"\n    tabIndex=\u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>step \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">?\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">-\u003C\u002Fspan>\u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>   \u002F\u002F roving tabindex, not 0 on every tab\n  &gt;\n    Account\n  \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">button\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F* …and arrow-key handling, and Home\u002FEnd, and focus movement on change, *\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-oper\">{\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F* and an aria-live region so the change is announced at all *\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">div\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-bash shj-oneline\" data-lang=\"bash\">\u003Cspan class=\"shj-func\">npm\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">install\u003C\u002Fspan> react-form-wizard-component\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizard\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\u002Fstyles.css\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Signup\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onComplete\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard.TabContent\u003C\u002Fspan> \u003Cspan class=\"shj-class\">title\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"Account\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n        \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">input\u003C\u002Fspan> \u003Cspan class=\"shj-class\">placeholder\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"Email\"\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard.TabContent\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard.TabContent\u003C\u002Fspan> \u003Cspan class=\"shj-class\">title\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"Profile\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n        \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">input\u003C\u002Fspan> \u003Cspan class=\"shj-class\">placeholder\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"Full name\"\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard.TabContent\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard.TabContent\u003C\u002Fspan> \u003Cspan class=\"shj-class\">title\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"Review\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n        \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">p\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>Looks good?\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">p\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n      \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard.TabContent\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003Cdiv>21\u003C\u002Fdiv>\u003Cdiv>22\u003C\u002Fdiv>\u003Cdiv>23\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizard\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizardSchema\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> schema\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizardSchema\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  initialData\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> plan\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"basic\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  steps\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"plan\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Plan\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">PlanPicker\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"billing\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Billing\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      \u003Cspan class=\"shj-cmnt\">\u002F\u002F The step disappears from the rail entirely when this is false.\n\u003C\u002Fspan>      condition\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> data \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> data\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>plan \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"premium\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">BillingFields\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"review\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Review\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      \u003Cspan class=\"shj-cmnt\">\u002F\u002F content can be a function of the current data.\n\u003C\u002Fspan>      content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> data \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">Review\u003C\u002Fspan> \u003Cspan class=\"shj-class\">data\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard\u003C\u002Fspan> \u003Cspan class=\"shj-class\">schema\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>schema\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-class\">onComplete\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">submit\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-oneline\" data-lang=\"tsx\">validate\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> data \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>accepted \u003Cspan class=\"shj-oper\">?\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Please accept the terms\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> z \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"zod\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizard\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> zodValidator \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> account \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> z\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">object\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  email\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> z\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">email\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"Enter a valid email\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  password\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> z\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">min\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">8\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"At least 8 characters\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F `pick` narrows the whole wizard data down to this step's slice.\n\u003C\u002Fspan>validate\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-func\">zodValidator\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>account\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pick\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"email\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"password\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> useForm \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-hook-form\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizard\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> hookFormValidator \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> form \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useForm\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> mode\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"onChange\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nvalidate\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-func\">hookFormValidator\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>form\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> fields\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"email\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"phone\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> useWizard \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> wizard \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useWizard\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  stepIds\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"account\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"profile\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"review\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  persist\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> key\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"signup\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  syncToUrl\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F writes ?step=2\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nwizard\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>currentStep;    \u003Cspan class=\"shj-cmnt\">\u002F\u002F 0-based cursor\n\u003C\u002Fspan>wizard\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>maxVisitedStep; \u003Cspan class=\"shj-cmnt\">\u002F\u002F the forward gate\n\u003C\u002Fspan>wizard\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>data;           \u003Cspan class=\"shj-cmnt\">\u002F\u002F shared data\n\u003C\u002Fspan>wizard\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">next\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\nwizard\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">goToId\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"review\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\nwizard\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">updateData\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> email \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F merges\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-diff shj-multiline\" data-lang=\"diff\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">  import \"react-form-wizard-component\u002Fstyles.css\";\n\u003Cspan class=\"shj-insert\">+ import \"react-form-wizard-component\u002Flegacy.css\";\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-deleted\">- &lt;FormWizard … \u002F&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-insert\">+ &lt;FormWizard variant=\"legacy\" … \u002F&gt;\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>                        \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F* auto: follows the page *\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard\u003C\u002Fspan> \u003Cspan class=\"shj-class\">colorScheme\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"system\"\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>   \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F* opt in to prefers-color-scheme *\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard\u003C\u002Fspan> \u003Cspan class=\"shj-class\">colorScheme\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"dark\"\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>     \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F* pin it *\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">.react-form-wizard\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  --\u003Cspan class=\"shj-var\">rfw-primary\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">#0e6f70\u003C\u002Fspan>;\n  --\u003Cspan class=\"shj-var\">rfw-radius\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">8\u003C\u002Fspan>\u003Cspan class=\"shj-var\">px\u003C\u002Fspan>\u003Cspan class=\"shj-num\">\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F Internals, for the curious — a Set of step ids, added to on a blocked next().\n\u003C\u002Fspan>showErrorOnTab\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>step\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>showErrorOnTab \u003Cspan class=\"shj-oper\">??\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>attemptedSteps\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">has\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>step\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>step\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>isValid\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">@import\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"tailwindcss\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">@import\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\u002Fstyles.css\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">@import\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\u002Ftailwind.css\"\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizard\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> tailwindPreset \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">FormWizard\u003C\u002Fspan> \u003Cspan class=\"shj-class\">unstyled\u003C\u002Fspan> \u003Cspan class=\"shj-class\">classNames\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>\u003Cspan class=\"shj-func\">tailwindPreset\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-class\">schema\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>schema\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F* Tailwind v4 *\u002F\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">@source\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"..\u002Fnode_modules\u002Freact-form-wizard-component\u002Fdist\u002F**\u002F*.js\"\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003Cdiv>21\u003C\u002Fdiv>\u003Cdiv>22\u003C\u002Fdiv>\u003Cdiv>23\u003C\u002Fdiv>\u003Cdiv>24\u003C\u002Fdiv>\u003Cdiv>25\u003C\u002Fdiv>\u003Cdiv>26\u003C\u002Fdiv>\u003Cdiv>27\u003C\u002Fdiv>\u003Cdiv>28\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> form \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> useForm\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-class\">Checkout\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> resolver\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-func\">zodResolver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>checkoutSchema\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> mode\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"onChange\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> schema\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizardSchema\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  steps\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"contact\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Contact\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">ContactFields\u003C\u002Fspan> \u003Cspan class=\"shj-class\">form\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>form\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      validate\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-func\">hookFormValidator\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>form\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> fields\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"email\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"phone\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"address\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Address\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">AddressFields\u003C\u002Fspan> \u003Cspan class=\"shj-class\">form\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>form\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      validate\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-func\">hookFormValidator\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>form\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> fields\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"street\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"city\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"postcode\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"payment\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Payment\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">PaymentFields\u003C\u002Fspan> \u003Cspan class=\"shj-class\">form\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>form\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      validate\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-func\">hookFormValidator\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>form\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> fields\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"cardNumber\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-class\">FormWizard\u003C\u002Fspan>\n  schema\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>schema\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  finishButtonText\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"Pay now\"\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F handleSubmit returns a handler, so invoke it rather than passing it through.\n\u003C\u002Fspan>  onComplete\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">void\u003C\u002Fspan> form\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">handleSubmit\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>submit\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-class\">FormWizard\u003C\u002Fspan>\n  schema\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>schema\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F \"session\" clears with the tab; \"local\" persists across restarts.\n\u003C\u002Fspan>  persist\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> key\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"onboarding\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> storage\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"session\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  syncToUrl\n\u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">steps\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"role\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Your role\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">RolePicker\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"team\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Team size\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    condition\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> data \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> data\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>role \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"manager\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">TeamSize\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"done\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Thanks\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">Thanks\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-tsx shj-multiline\" data-lang=\"tsx\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003Cdiv>21\u003C\u002Fdiv>\u003Cdiv>22\u003C\u002Fdiv>\u003Cdiv>23\u003C\u002Fdiv>\u003Cdiv>24\u003C\u002Fdiv>\u003Cdiv>25\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizard\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> zodValidator\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">type\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizardSchema\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\"\u003C\u002Fspan>;\n\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"react-form-wizard-component\u002Fstyles.css\"\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> schema\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-class\">FormWizardSchema\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  initialData\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> plan\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"basic\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  steps\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"plan\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Plan\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">PlanPicker\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"billing\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Billing\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      condition\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> data \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> data\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>plan \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"premium\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>     \u003Cspan class=\"shj-cmnt\">\u002F\u002F cursor: branch\n\u003C\u002Fspan>      content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">BillingFields\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      validate\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-func\">zodValidator\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>billing\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> pick\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"card\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>  \u003Cspan class=\"shj-cmnt\">\u002F\u002F gate: this step only\n\u003C\u002Fspan>    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"review\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> title\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"Review\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> content\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> data \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">Review\u003C\u002Fspan> \u003Cspan class=\"shj-class\">data\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">={\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-class\">FormWizard\u003C\u002Fspan>\n  schema\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>schema\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  persist\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> key\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"checkout\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>   \u003Cspan class=\"shj-cmnt\">\u002F\u002F data: survives a refresh\n\u003C\u002Fspan>  syncToUrl                       \u003Cspan class=\"shj-cmnt\">\u002F\u002F cursor: survives a shared link\n\u003C\u002Fspan>  theme\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> primaryColor\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"#0e6f70\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  onComplete\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">submit\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":879},[]]