Retool's Wizard component (also called Stepped Container) provides a built-in multi-step UI with a numbered progress bar. Add a Form component to each step for per-step validation. Use the component's built-in Next/Back navigation or control it programmatically with wizard1.setStep(). Access the current step index via {{ wizard1.currentStepIndex }}.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Intermediate |
| Time required | 25-35 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Building Step-by-Step Wizards with Retool's Wizard Component
Retool's Wizard component (also surfaced as 'Stepped Container' in some versions) is a purpose-built multi-step navigation container with a built-in numbered progress indicator. Unlike the Tabbed Container approach used in the multi-step-forms tutorial, the Wizard component includes visual step indicators, step labels, and native Next/Back buttons — reducing the amount of custom wiring needed.
The Wizard component is distinct from the multi-step-forms pattern: the Wizard uses the built-in component with its native controls; multi-step-forms uses a Tabbed Container with manual Temporary State management. The Wizard approach is faster to set up and provides a polished UI out of the box. Use it when you want a standard wizard experience. Use the Tabbed Container approach when you need full custom control over navigation logic and layout.
This tutorial builds a 4-step onboarding wizard: Account Info, Plan Selection, Payment Details, and Review & Launch. Each step has its own Form component. Step validation gates advancement. The final Review step shows a summary before submission.
Prerequisites
- Familiarity with Form component validation
- Basic knowledge of JS Queries and Temporary State
- A database resource configured for the final submission step
Step-by-step guide
Add the Wizard component and configure steps
Search for 'Wizard' or 'Stepped Container' in the component panel search box. Drag it onto the canvas. In the Inspector's General section, configure the number of steps by adding or removing step tabs. Name each step in the Steps array: 'Account Info', 'Plan Selection', 'Payment Details', 'Review & Launch'. The Wizard renders a numbered progress bar at the top showing all steps, with the current step highlighted. The step labels appear as tooltip text or header text depending on the Wizard version.
Expected result: A Wizard component appears with 4 numbered steps and a progress indicator. Each step has its own content area.
Add a Form component to each wizard step
Inside each Wizard step, add a Form component: step1Form in Step 1, step2Form in Step 2, step3Form in Step 3. Step 4 (Review) uses Text components for read-only display — no Form needed. Configure the fields for each step: Step 1 — firstNameInput, lastNameInput, emailInput (Required, Email pattern); Step 2 — planSelect (Required) with plan options; Step 3 — cardNameInput, cardNumberInput (these are display-only in this tutorial — actual payment should use a Stripe integration).
Expected result: Each wizard step contains a properly named Form component with appropriate input fields and validation rules.
Override Next button behavior for step validation
The Wizard component's built-in Next button advances to the next step without validation by default. To gate advancement on validation, override the Next button's behavior. In the Wizard component's Inspector, find the 'Before step change' event or override the Next button's onClick by configuring a 'Validate before advancing' option. If no built-in gate exists in your Retool version, add a custom Next Button component below each step's form and hide the Wizard's built-in navigation.
1// JS Query: validateStep1AndAdvance2// Trigger: Custom 'Next' button in Step 134const isValid = step1Form.validate();56if (!isValid) {7 utils.showNotification({8 title: 'Please complete this step',9 description: 'Fill in all required fields before continuing.',10 notificationType: 'warning',11 });12 return;13}1415// Advance to step 2 (0-indexed)16wizard1.setStep(1);1718// Access current step index anywhere:19// {{ wizard1.currentStepIndex }}20// 0 = Step 1, 1 = Step 2, 2 = Step 3, 3 = Step 4Expected result: Clicking Next on Step 1 with invalid data shows errors and stays on Step 1. Valid data advances to Step 2.
Store accumulated step data in Temporary State
Create a Temporary State named wizardData with default value {} to accumulate data across steps. After validating each step, store its Form data into wizardData before advancing. The Review step and final submission read from wizardData instead of individual Form components (which may not be in the active step when the last step renders).
1// JS Query: validateStep2AndAdvance2// Trigger: Custom 'Next' button in Step 234const isValid = step2Form.validate();5if (!isValid) {6 utils.showNotification({7 title: 'Please select a plan',8 notificationType: 'warning',9 });10 return;11}1213// Accumulate step 2 data14await wizardData.setValue({15 ...wizardData.value,16 plan: step2Form.data.planSelect,17 billingCycle: step2Form.data.billingCycleSelect,18});1920wizard1.setStep(2);2122// In step 3 Next handler, also store step 1 and 3 data:23// await wizardData.setValue({24// firstName: step1Form.data.firstNameInput,25// lastName: step1Form.data.lastNameInput,26// email: step1Form.data.emailInput,27// plan: wizardData.value.plan,28// billingCycle: wizardData.value.billingCycle,29// cardName: step3Form.data.cardNameInput,30// })Expected result: wizardData.value accumulates data from each step as the user advances through the wizard.
Build the Review step with a summary display
In Step 4 (Review & Launch), add Text components that display the accumulated wizardData.value fields for the user to review before submitting. Use descriptive labels alongside the data values. Add a 'Back' button that calls wizard1.setStep(2) to go back to Step 3, and a 'Launch Account' button that triggers the final submitWizard JS Query.
1// Review step Text component values:23// Account info summary:4{{ `Name: ${wizardData.value.firstName} ${wizardData.value.lastName}` }}5{{ `Email: ${wizardData.value.email}` }}67// Plan summary:8{{ `Plan: ${wizardData.value.plan} (${wizardData.value.billingCycle})` }}910// Formatted plan price:11{{ wizardData.value.plan === 'pro' ? '$29/month' 12 : wizardData.value.plan === 'enterprise' ? '$99/month'13 : '$9/month' }}1415// Hidden confirmation checkbox:16// confirmCheckbox (required) — Hidden: false, Required: true17// Label: 'I agree to the Terms of Service'18// form4.validate() checks this before submissionExpected result: Step 4 shows a complete summary of all wizard data entered across Steps 1-3.
Submit the wizard and reset on success
Create the final JS Query named submitWizard. It reads from wizardData.value (accumulated across all steps) and calls the creation mutation. On success, show a completion notification, reset the wizard to Step 1, and clear wizardData. Optionally navigate to a success page or dashboard.
1// JS Query: submitWizard2// Trigger: 'Launch Account' button in Step 434const data = wizardData.value;56// Validate review step (e.g., ToS checkbox)7const reviewValid = confirmCheckbox.value === true;8if (!reviewValid) {9 utils.showNotification({10 title: 'Agreement required',11 description: 'Please accept the Terms of Service to continue.',12 notificationType: 'warning',13 });14 return;15}1617try {18 await createAccount.trigger({19 additionalScope: {20 firstName: data.firstName,21 lastName: data.lastName,22 email: data.email,23 plan: data.plan,24 billingCycle: data.billingCycle,25 }26 });2728 utils.showNotification({29 title: 'Account created!',30 description: `Welcome, ${data.firstName}! Your ${data.plan} account is ready.`,31 notificationType: 'success',32 });3334 // Reset wizard35 await wizardData.setValue({});36 wizard1.setStep(0);37 [step1Form, step2Form, step3Form].forEach(f => f.reset());3839} catch (err) {40 utils.showNotification({41 title: 'Account creation failed',42 description: err.message,43 notificationType: 'error',44 });45 throw err;46}Expected result: Clicking Launch Account creates the record, shows a success notification, and resets the wizard to Step 1 ready for the next user.
Complete working example
1// validateAndAdvance — generic per-step validation and advancement2// Replace 'currentStepForm' and field mappings for each step34const currentStep = wizard1.currentStepIndex;56// Step-specific form and data config7const stepConfigs = [8 {9 form: step1Form,10 dataKey: 'step1',11 fields: {12 firstName: step1Form.data.firstNameInput,13 lastName: step1Form.data.lastNameInput,14 email: step1Form.data.emailInput,15 }16 },17 {18 form: step2Form,19 dataKey: 'step2',20 fields: {21 plan: step2Form.data.planSelect,22 billingCycle: step2Form.data.billingCycleSelect,23 }24 },25 {26 form: step3Form,27 dataKey: 'step3',28 fields: {29 cardName: step3Form.data.cardNameInput,30 }31 },32];3334const config = stepConfigs[currentStep];3536if (!config) {37 // Already on final step — submit38 return;39}4041// Validate current step42const isValid = config.form.validate();43if (!isValid) {44 utils.showNotification({45 title: 'Please complete this step',46 description: 'All required fields must be filled.',47 notificationType: 'warning',48 });49 return;50}5152// Accumulate data53await wizardData.setValue({54 ...wizardData.value,55 ...config.fields,56});5758// Advance59wizard1.setStep(currentStep + 1);Common mistakes when creating Wizard Interfaces in Retool
Why it's a problem: Reading form data from step1Form.data on the final submit step — if Step 1 is not the active step, its form components may not have the user's values
How to avoid: Accumulate all form data into a Temporary State (wizardData) as each step is completed. Read from wizardData.value on the final submission, not from individual step form components.
Why it's a problem: Using wizard1.setStep(2) to go to Step 3 (1-indexed thinking) — this actually skips Step 2 and goes to Step 3 (0-indexed)
How to avoid: wizard1.setStep() uses 0-based indexing. Step 1 = index 0, Step 2 = index 1, Step 3 = index 2, Step 4 = index 3. To advance by one: wizard1.setStep(wizard1.currentStepIndex + 1).
Why it's a problem: Expecting wizard1.currentStepIndex to update synchronously immediately after calling wizard1.setStep() in the same JS Query
How to avoid: setStep() triggers a UI re-render. If you need to use the new step index immediately after setting it, use the value you computed (currentStep + 1) rather than re-reading wizard1.currentStepIndex.
Why it's a problem: Not awaiting wizardData.setValue() before calling wizard1.setStep() — the wizard advances before data is stored, causing the review step to show stale or partial data
How to avoid: Always await wizardData.setValue(newData) before calling wizard1.setStep(nextIndex).
Best practices
- Use the Wizard component's built-in progress indicator rather than building a custom one — it stays in sync with wizard1.currentStepIndex automatically
- Store step data in a Temporary State object as users advance — do not rely on reading step forms that are not in the active step
- Validate each step before advancing using stepNForm.validate() — catching errors per-step is a much better UX than showing all errors on the review step
- Add a Back button to each step (hidden on Step 1) and restore previously entered data with setData() so users do not have to re-enter values
- Use 0-based indexing for wizard1.setStep() — Step 1 is index 0, Step 4 is index 3
- Show a progress percentage or 'Step N of M' indicator as additional context beyond the numbered step indicators
- Reset the wizard completely on success (wizard1.setStep(0), clear wizardData, reset all step forms) to prepare for the next use
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a 4-step onboarding wizard in Retool using the Wizard component (wizard1). The steps are: Step 1 (step1Form: firstNameInput, lastNameInput, emailInput), Step 2 (step2Form: planSelect), Step 3 (step3Form: cardNameInput), Step 4 (review). I have a Temporary State named wizardData (default {}). Write: (1) a JS Query named validateAndAdvance that validates the current step's form using wizard1.currentStepIndex, stores the form data in wizardData, and calls wizard1.setStep() to advance, (2) the Review step Text component expressions that display wizardData.value fields, (3) a submitWizard JS Query that reads from wizardData.value and resets the wizard.
I have a Wizard component in Retool. How do I programmatically go to a specific step? What is the API — wizard1.setStep()? What is the first step index (0 or 1)? Also, how do I read which step the user is currently on to use in an if/else condition?
Frequently asked questions
Can I allow users to jump directly to any step without going through previous steps?
Yes — call wizard1.setStep(targetIndex) from any button or event handler. However, if earlier steps have Required validation, submitting from a later step without completing them will fail. For free navigation, remove per-step validation gates or make all fields optional with custom validation only on final submission.
How do I mark a wizard step as completed with a checkmark in the progress indicator?
This depends on your Retool version. In some versions, the Wizard component automatically marks steps with a checkmark when the user advances past them. In others, you configure step 'status' in the Inspector. Check your Retool version's Wizard component documentation for the step completion status API.
What is the difference between the Wizard component and the Tabbed Container for multi-step forms?
The Wizard component has a built-in numbered progress indicator, step labels, and native navigation — less setup, more standardized look. The Tabbed Container approach (from the multi-step-forms tutorial) requires manual Temporary State for step tracking and custom buttons, but gives full control over layout, step count, and navigation logic. Use Wizard for standard wizard UX; use Tabbed Container when you need custom control.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation