Introduction
@saastro/forms is a dynamic form system for React applications. It combines the power of React Hook Form, Zod validation, and dependency injection to create type-safe, configurable forms with 34 field types.
Why @saastro/forms?
Build complex forms with minimal code, maximum type safety, and full UI flexibility.
Traditional form libraries make you choose: either you get type safety and write a lot of boilerplate, or you get convenience and lose type safety. @saastro/forms gives you both:
- Zero-config mode — Auto-discover shadcn components, no Provider needed
- Type-safe configuration — Full TypeScript support with autocomplete
- JSON-serializable — Store form configs in databases, send over APIs
- Declarative validation — Zod schemas or simple rules objects
- UI agnostic — Bring your own components via dependency injection
- 34 field types — Text, select, slider, date, file, repeater, hidden fields, and more
- Multi-step forms — Built-in wizard support with conditional step routing and progress tracking
- Conditional logic — Show/hide/disable fields based on values
- i18n built in — Per-locale overlays for labels, options, and messages (see Internationalization)
Key Features
1. Zero-Config Mode (Recommended)
The simplest way to get started — just pass your components:
import { Form, FormBuilder } from '@saastro/forms';
// Auto-discover all shadcn components with Vite's glob
const uiComponents = import.meta.glob('@/components/ui/*.tsx', { eager: true });
const config = FormBuilder.create('contact')
.addField('name', (f) => f.type('text').label('Name').required())
.addField('email', (f) => f.type('email').label('Email').required().email())
.addStep('main', ['name', 'email'])
.onSuccess((values) => console.log(values)) // fires after every successful submit
.build();
function ContactForm() {
return <Form config={config} components={uiComponents} />;
}
2. FormBuilder API
Fluent, chainable API for building forms:
const config = FormBuilder.create('contact')
.layout('manual') // Fixed 12-column grid
.columns(12)
.addField(
'name',
(f) =>
f.type('text').label('Name').required('Name is required').columns({ default: 12, md: 6 }), // Full width mobile, half on md+
)
.addField('email', (f) =>
f
.type('email')
.label('Email')
.required()
.email('Please enter a valid email')
.columns({ default: 12, md: 6 }),
)
.addStep('main', ['name', 'email'])
.build();
3. JSON Configuration
Same forms, defined as JSON (perfect for CMSes and visual builders):
{
"formId": "contact",
"layout": { "mode": "manual", "columns": 12 },
"fields": {
"name": {
"type": "text",
"label": "Name",
"schema": { "required": true, "requiredMessage": "Name is required" },
"columns": { "default": 12, "md": 6 }
},
"email": {
"type": "email",
"label": "Email",
"schema": { "required": true, "format": "email" },
"columns": { "default": 12, "md": 6 }
}
},
"steps": { "main": { "fields": ["name", "email"] } }
}
Note the shape: formId (not id) identifies the form, grid options like columns live under layout, and steps is a record keyed by step id — not an array.
4. Dependency Injection
Use any UI library. Pass components directly or use a Provider:
// Zero-config (recommended)
<Form config={config} components={uiComponents} />
// Or explicit components
<Form
config={config}
components={{
Input: MyCustomInput,
Button: MyCustomButton,
}}
/>
// Or legacy Provider pattern
<ComponentProvider components={registry}>
<Form config={config} />
</ComponentProvider>
Field Types
@saastro/forms includes 34 field types out of the box — 30 data and content types plus 4 action types:
| Category | Fields |
|---|---|
| Text | text, email, tel, url, password, number, textarea, input-group, currency |
| Selection | select, native-select, combobox, command, radio, button-radio |
| Toggle | checkbox, switch, checkbox-group, switch-group, button-checkbox |
| Special | slider, range, date, daterange, otp, file, repeater, hidden, button-card, html |
| Actions | button, submit, next, back |
A few notes:
currencyrenders identically toinput-group, andrangeidentically toslider— they are aliases with the same props.commandrenders the same searchable popover UI ascombobox.hiddenrenders no UI; its value is filled by a resolver (timestamp, URL parameter, referrer, and more) — see Hidden Fields.
When to Use @saastro/forms
Perfect for:
- Dynamic forms that change based on configuration
- Visual form builders and CMSes
- Multi-step wizards and onboarding flows
- Applications that need consistent form UX
- Teams that want type-safe form development
Consider alternatives if:
- You need a single, static form (just use React Hook Form directly)
- You’re not using React
- You need server-side form handling (consider Remix forms)
Architecture
@saastro/forms
├── FormBuilder # Fluent API for building configs
├── Form # Main render component (zero-config ready)
├── ComponentProvider # DI container for UI components (optional)
├── PluginManager # Extensibility (analytics, autosave, etc.)
├── Validation # Zod schema compiler from JSON rules
└── Renderers # Field type implementations
Ready to Start?
- Quickstart — build and render your first form in minutes.
- Installation — full setup guide, including the CLI.
- Field Types — interactive examples of every field.