form_wizard 🧙♂️
The most performant, customizable, lightweight, and powerful form builder for Flutter.
form_wizard is built for every serious Flutter form, from simple login screens to complex onboarding, checkout, KYC, dashboard, survey, dynamic-array, and multi-step workflows.
Under the hood, form_wizard uses Riverpod for surgical, field-level reactivity. In your app, it feels dependency-free: no ProviderScope, no Riverpod setup, no state-management lock-in. Install it, define your fields, and ship fast forms that stay fast.
Why form_wizard?
Flutter forms should not become slower, messier, or harder to maintain as they grow. form_wizard gives you the performance, validation engine, dynamic UI, templates, and customization needed to build production forms without rebuilding the world on every keystroke.
form_wizard is built for Flutter teams who want forms that feel simple to create, powerful to extend, and fast no matter how complex the workflow becomes.
form_wizard brings the most in-demand form-builder features into one lightweight package:
| In-demand feature | Built into form_wizard |
|---|---|
| High-performance forms | Only the field being edited rebuilds |
| Server-side checks | Debounced async validators with stale-result cancellation |
| Cross-field rules | Dependency-aware validation for password match, date ranges, totals, and more |
| Conditional fields | Show/hide fields reactively based on other values |
| Repeatable groups | Dynamic field arrays with add, remove, reorder, and stable item values |
| Multi-step flows | FormWizardStepper with active-step-only rendering |
| Ready-made forms | Login, Signup, OTP, Address, and Payment templates |
| Common inputs | Email, phone, password, OTP, address, ZIP, country, and name presets |
| Production UX | Dirty/touched/validating state, typed values, and focus-first-invalid submit |
| Full customization | Custom fields, custom decorations, custom buttons, and custom stepper UI |
Use it for a login screen, checkout, KYC flow, dashboard editor, survey, onboarding wizard, team-member array, or any form that should stay fast as it grows.
Demo
Fine-Grained Form UI

Real-Time Validation

Custom Field Validation

What's New in v0.2.0
v0.2.0 upgrades form_wizard from a fast form builder into a production-ready validation engine.
| Upgrade | Why it matters |
|---|---|
| Debounced async validators | Check usernames, emails, invite codes, coupon codes, or server rules without blocking the UI |
| Stale async cancellation | Fast typing cannot let an old async response overwrite the newest value |
| Dependency-aware validation | Confirm password, start/end date, min/max amount, and related fields revalidate correctly |
| Dirty/touched/submitted/validating state | Build polished validation UX without whole-form subscriptions |
| Typed value transformers | Keep text input fast, submit clean typed values |
| Focus first invalid field | Failed submit takes users directly to the field they need to fix |
| Richer field options | Input formatters, autofill hints, keyboard actions, max length, read-only/enabled state, and more |
| Safer templates | Built-in templates now own and dispose internal controllers correctly |
Install
dependencies:
form_wizard: ^0.2.0
import 'package:form_wizard/form_wizard.dart';
No app-level Riverpod setup is required.
Quick Start
final controller = FormWizardController();
FormWizard(
controller: controller,
fields: [
FormWizard.emailField(),
FormWizard.passwordField(),
],
onSubmit: (values) {
print(values);
},
)
That is enough to get validation, submit handling, fine-grained rebuilds, and internal state management.
Performance Architecture
form_wizard stores form values, errors, visibility, arrays, and validation state in one immutable FormState.
Each field listens only to its own slice:
ref.watch(formStateProvider.select((state) => state.values[fieldName]));
ref.watch(formStateProvider.select((state) => state.errors[fieldName]));
ref.watch(fieldValidatingProvider(fieldName));
When a user edits email, the email field reacts. Other fields stay still unless they explicitly depend on email.
What does not rebuild unnecessarily?
- Sibling fields when one field changes
- Hidden conditional fields
- Inactive stepper steps
- Field-array siblings unless the item list changes
- Submit controls unless validity/pending state changes
- Unrelated cross-field validators
Dependency-aware updates
If confirm_password declares:
validationDependsOn: ['password']
then changing password revalidates password and confirm_password, not the whole form.
Feature Overview
Core
- Fine-grained state updates
- Internal Riverpod 3 powered store
- No external
ProviderScoperequired FormWizardControllerwithValueListenables- Sync validation
- Debounced async validation
- Cross-field validation
- Hidden-field-aware validity
- Dirty, touched, submitted, and validating state
- Focus/scroll to first invalid field
Fields
- Text
- Password
- Number
- Dropdown
- Date
- Custom widgets
- Input formatters
- Autofill hints
- Keyboard actions
- Typed value transformers
- Custom decorations
Advanced
- Conditional visibility
- Dynamic field arrays
- Add/remove/reorder repeatable groups
- Multi-step forms with
FormWizardStepper - Built-in templates
- Field presets
Async Validation
Use async validators for server-backed or expensive checks.
FormWizardFieldModel(
name: 'username',
label: 'Username',
type: FieldType.text,
validators: [Validators.required()],
asyncValidationDebounce: const Duration(milliseconds: 300),
asyncValidators: [
(value, context) async {
final available = await api.isUsernameAvailable(value ?? '');
return available ? null : 'Username is already taken';
},
],
)
Submit with async validators:
await controller.submitFormAsync((values) {
saveProfile(values);
});
The async engine is debounced per field and ignores stale responses.
Cross-Field Validation
Use contextValidators when a field depends on other values.
FormWizardFieldModel(
name: 'confirm_password',
label: 'Confirm Password',
type: FieldType.password,
validators: [Validators.required()],
contextValidators: [
Validators.matchesField(
'password',
message: 'Passwords do not match',
),
],
validationDependsOn: ['password'],
)
The dependency list is important. It tells form_wizard exactly which fields should revalidate together.
Conditional Visibility
Show or hide fields based on other values without waking the whole form.
FormWizardFieldModel(
name: 'company_name',
label: 'Company Name',
type: FieldType.text,
validators: [Validators.required()],
visibleWhenDependsOn: ['account_type'],
visibleWhen: (values) => values['account_type'] == 'Business',
)
Hidden fields keep their values, but they do not block submit or form validity.
Dynamic Field Arrays
Use field arrays for repeatable groups: phone numbers, addresses, team members, invoices, dependents, emergency contacts, and more.
FormWizard(
controller: controller,
fields: const [],
fieldArrays: [
FormWizardFieldArrayModel(
name: 'team_members',
label: 'Team Members',
initialItemCount: 1,
minItems: 1,
maxItems: 10,
fieldBuilder: (item) => [
FormWizardFieldModel(
name: item.fieldName('name'),
label: 'Member ${item.index + 1} Name',
type: FieldType.text,
validators: [Validators.required()],
),
FormWizardFieldModel(
name: item.fieldName('email'),
label: 'Member ${item.index + 1} Email',
type: FieldType.email,
validators: [Validators.email()],
),
],
),
],
)
Each item receives a stable ID, so values stay attached to the right row during reorder operations.
controller.addFieldArrayItem('team_members');
controller.removeFieldArrayItem('team_members', itemId);
controller.reorderFieldArrayItem('team_members', oldIndex, newIndex);
Read grouped values:
final members = controller.getFieldArrayValues('team_members');
Multi-Step Forms
FormWizardStepper creates isolated multi-step forms. Only the active step's fields are in the widget tree.
FormWizardStepper(
steps: [
FormWizardStep(
title: 'Personal',
fields: [
FormWizard.nameField(name: 'name'),
FormWizard.emailField(name: 'email'),
],
),
FormWizardStep(
title: 'Address',
fields: [
FormWizard.streetField(name: 'street'),
FormWizard.cityField(name: 'city'),
],
),
],
onFinish: (values) {
print(values);
},
)
Stepper features:
- Active-step-only rendering
- Per-step validation
- Persistent data across steps
- Optional
onStepChanged - Custom UI with
stepBuilder - Material stepper by default
Built-In Templates
Ship common forms in minutes, then override anything.
Login
LoginForm(
identityType: FormWizardIdentityType.email,
rememberMe: true,
forgotPasswordLink: () => openForgotPassword(),
onLogin: (identity, password) {
auth.login(identity, password);
},
)
Signup
SignupForm(
identityType: FormWizardIdentityType.email,
requireTermsAcceptance: true,
onSignup: (name, identity, password) {
auth.createAccount(name, identity, password);
},
)
OTP Verification
OTPVerificationForm(
otpLength: 6,
resendCooldownSeconds: 30,
onVerify: verifyOtp,
onResend: resendOtp,
)
Address
AddressForm(
includeState: true,
onSubmit: saveAddress,
)
Payment
PaymentForm(
submitLabel: 'Pay Now',
onSubmit: processPayment,
)
Field Presets
Use presets when you want composable building blocks instead of full templates.
FormWizard(
controller: controller,
fields: [
FormWizard.nameField(),
FormWizard.emailField(
asyncValidators: [
(value, context) async {
final exists = await api.emailExists(value ?? '');
return exists ? 'Email already exists' : null;
},
],
),
FormWizard.passwordField(),
FormWizard.phoneField(),
FormWizard.streetField(),
FormWizard.cityField(),
FormWizard.zipField(),
FormWizard.countryDropdown(),
FormWizard.otpField(length: 6),
],
onSubmit: save,
)
Typed Submit Values
Raw field input stays simple and fast. Transform values only when you need typed output.
FormWizardFieldModel(
name: 'age',
label: 'Age',
type: FieldType.number,
validators: [Validators.required(), Validators.number()],
valueTransformer: (value, context) {
return int.tryParse(value?.toString() ?? '');
},
)
await controller.submitFormAsync(
(values) {
print(values['age']); // int?
},
transformValues: true,
);
Custom Field Widgets
Use customBuilder when you want total control.
FormWizardFieldModel(
name: 'budget',
label: 'Budget',
type: FieldType.custom,
validators: [
(value) {
final amount = double.tryParse(value ?? '');
return amount != null && amount >= 1000
? null
: 'Minimum budget is 1000';
},
],
customBuilder: (controller, errorText, onChanged) {
final value = double.tryParse(controller.text) ?? 1000;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Slider(
min: 1000,
max: 10000,
divisions: 9,
value: value,
onChanged: (next) {
controller.text = next.toStringAsFixed(0);
onChanged(controller.text);
},
),
if (errorText != null)
Text(
errorText,
style: const TextStyle(color: Colors.red),
),
],
);
},
)
Controller API
final controller = FormWizardController();
controller.updateFieldValue('email', 'ava@example.com');
controller.validateField('email');
await controller.validateFieldAsync('username');
controller.validateForm();
await controller.validateFormAsync();
controller.formData;
controller.transformedFormData;
controller.isFormValid;
controller.isValidating;
controller.fieldErrors;
controller.focusFirstInvalidField();
controller.dispose();
Supported Field Types
| Type | Use case |
|---|---|
FieldType.text |
Standard text input |
FieldType.email |
Email keyboard and email validation |
FieldType.password |
Obscured password input |
FieldType.number |
Numeric input |
FieldType.dropdown |
Select from options |
FieldType.date |
Date picker with formatting |
FieldType.custom |
Fully custom widget |
Validation Toolkit
Validators.required()
Validators.email()
Validators.minLength(8)
Validators.maxLength(64)
Validators.number()
Validators.phone()
Validators.exactLength(6)
Validators.regex(RegExp(r'^[a-z0-9_]+$'))
Validators.matchesField('password')
Custom validators are just functions:
validators: [
(value) => value == 'magic' ? null : 'Only magic is accepted',
]
API Docs
Contributing
Contributions are welcome.
Good contribution areas:
- Additional field presets
- More validation helpers
- Accessibility improvements
- Better examples
- Performance benchmarks
- Documentation improvements
Before opening a pull request:
dart format .
flutter analyze
flutter test
License
form_wizard is released under the MIT License. See LICENSE.
Built with Flutter, Riverpod, and an unreasonable obsession with form performance by Tanay.