quantique-forms
v1.0.6
Published
JSON-driven dynamic form engine for React — 14 field types, full validation via quantique-field-validator, CSS-variable theming
Maintainers
Readme
quantique-forms
JSON-driven dynamic form engine for React.
Describe your entire form as a plain JavaScript/TypeScript object.DynamicFormrenders the correct field, applies smart input filters, wires up validation, and hands you a clean typed object on submit — no hand-rolled JSX, no per-field boilerplate.
Table of contents
- What is this?
- Why use it?
- Installation
- Quick start — 60 seconds
- All 14 field types with examples
- Smart field-type constraints
- Override auto-constraints
- Full ValidationConfig reference
- Date validation rules
- Sections — grouping fields visually
- Theming with CSS variables
- Inter-field reactivity
- Pre-filling default values
- Custom submit / action buttons
- DynamicForm prop reference
- FieldConfig full reference
- TypeScript types
- Real-world example — vehicle registration form
- Changelog
1. What is this?
Most React forms require you to write the same boilerplate over and over — one block of JSX per field, with manual label, input, error, register, pattern, and accessibility wiring:
// WITHOUT quantique-forms — you write this for EVERY field
<div>
<label htmlFor="pan">PAN Card *</label>
<input
id="pan"
maxLength={10}
onChange={(e) => setValue('pan', e.target.value.toUpperCase())}
{...register('pan', {
required: 'PAN is required',
pattern: { value: /^[A-Z]{5}[0-9]{4}[A-Z]$/, message: 'Invalid PAN' },
})}
/>
{errors.pan && <span className="error">{errors.pan.message}</span>}
</div>With quantique-forms, you write a plain object instead:
// WITH quantique-forms — describe what you want, library handles the rest
{
id: 1,
title: 'PAN Card',
variableName: 'pan',
type: 'textField',
placeholder: 'ABCDE1234F',
validations: {
isRequired: true,
fieldType: 'pan', // ← auto-uppercase, max 10, alphanumeric only, format check
},
}That one object produces: a labelled input, auto-uppercasing on every keystroke, character filter (alphanumeric only), hard max-length cap, PAN format validation, required check, and an inline error message — all styled consistently.
2. Why use it?
| Challenge | Without this library | With quantique-forms |
|---|---|---|
| Building a form with 10 fields | ~300 lines of JSX + hooks | ~50 lines of JSON |
| Adding a new field | Edit JSX, register, add error UI | Add one object to the array |
| Form from an API / CMS / DB | Complex mapping code | Pass the JSON array directly |
| Theming all 14 field types consistently | Override CSS in 20+ places | Change one CSS variable |
| Indian field validation (PAN, GST, IFSC, Aadhaar…) | Write/test regex by hand | Set fieldType: 'pan' |
| Smart input (digits-only, uppercase, numeric keyboard) | Custom onChange per field | Automatic via fieldType |
| Vehicle form (chassis, engine, reg code, policy no.) | Research each format separately | Built-in field types |
| Prevent invalid characters from being typed | Custom onKeyDown filter per field | Automatic via fieldType |
3. Installation
npm install quantique-forms
# or
yarn add quantique-forms
# or
pnpm add quantique-formsPeer dependencies (install if not already in your project):
npm install react react-dom
react-hook-formandquantique-field-validatorship as dependencies insidequantique-forms— you do not need to install them separately unless you use them directly.
Node.js ≥ 18, React ≥ 18, TypeScript 5.x (optional)
4. Quick start — 60 seconds
Step 1 — Import the stylesheet once (at the very top of your app)
// main.tsx OR App.tsx OR layout.tsx — wherever your app starts
import 'quantique-forms/styles';You only need this once per application. If you skip this, the fields will render without any styling.
Step 2 — Describe your fields and render DynamicForm
import { DynamicForm } from 'quantique-forms';
import type { FieldConfig } from 'quantique-forms';
const fields: FieldConfig[] = [
{
id: 1,
title: 'Full Name',
variableName: 'fullName',
type: 'textField',
placeholder: 'Jane Smith',
validations: { isRequired: true, minLength: 2, maxLength: 80 },
},
{
id: 2,
title: 'Email Address',
variableName: 'email',
type: 'email',
placeholder: '[email protected]',
validations: {
isRequired: true,
fieldType: 'email',
errorMessages: { invalid: 'Please enter a valid email address.' },
},
},
{
id: 3,
title: 'Role',
variableName: 'role',
type: 'dropdown',
placeholder: 'Select a role…',
options: [
{ value: 'admin', label: 'Admin' },
{ value: 'editor', label: 'Editor' },
{ value: 'viewer', label: 'Viewer' },
],
validations: { isRequired: true },
},
];
export default function MyForm() {
return (
<DynamicForm
fields={fields}
onSubmit={(data) => console.log(data)}
// Logs: { fullName: 'Jane Smith', email: '[email protected]', role: 'editor' }
renderActions={(isSubmitting) => (
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving…' : 'Save'}
</button>
)}
/>
);
}What happens automatically:
- The form validates on submit
- Required field errors appear inline
- Email format is checked
onSubmitis only called when every field passes validation- The submitted
dataobject keys match each field'svariableName
5. All 14 field types with examples
Every field config needs at minimum: id, title, variableName, and type. Everything else is optional.
textField
A standard single-line text input. The most versatile field type — works for names, addresses, reference codes, ID numbers, and anything else. Pair it with fieldType in validations to get smart constraints.
{
id: 1,
title: 'Full Name',
variableName: 'fullName',
type: 'textField',
placeholder: 'Jane Smith',
info: 'Enter your legal name as it appears on your government ID.',
validations: {
isRequired: true,
isRequiredError: 'Please enter your full name.',
minLength: 2,
maxLength: 80,
minLengthError: 'Name must be at least 2 characters.',
maxLengthError: 'Name cannot exceed 80 characters.',
},
}What the user sees: A labelled text box. Required fields show a red *. An optional info string appears below the label as a hint. Validation errors appear below the input after the user blurs or submits.
email
A text input with type="email". On mobile, this shows the email-optimised keyboard (with @ key visible). Use fieldType: 'email' to add format validation.
{
id: 2,
title: 'Email Address',
variableName: 'email',
type: 'email',
placeholder: '[email protected]',
validations: {
isRequired: true,
fieldType: 'email',
errorMessages: {
required: 'Email address is required.',
invalid: 'Please enter a valid email (e.g. [email protected]).',
},
},
}password
A password input with a built-in show/hide eye icon. The user can click the eye to reveal what they typed. Pair with fieldType: 'password' and minLength for strength enforcement.
{
id: 3,
title: 'Password',
variableName: 'password',
type: 'password',
placeholder: 'Min 8 characters',
validations: {
isRequired: true,
minLength: 8,
fieldType: 'password',
errorMessages: {
required: 'Please create a password.',
invalid: 'Password must contain at least one letter and one number.',
},
},
}multiTextField (textarea)
A resizable multi-line text area. Control its default height using rowSize. Ideal for comments, notes, addresses, and longer free-form text.
{
id: 4,
title: 'Additional Notes',
variableName: 'notes',
type: 'multiTextField',
placeholder: 'Enter any additional information…',
rowSize: 5,
validations: { maxLength: 500 },
}rowSize — number of visible text rows (height). Default is 3. Set higher for longer content (e.g. rowSize: 8 for a full description field).
A live character counter (0 / 500) appears in the bottom-right corner when maxLength is set.
numberField
An integer (whole number) input. Digits only — no decimals, no letters. On mobile, shows the numeric keyboard. Supports min/max value checks.
{
id: 5,
title: 'Age',
variableName: 'age',
type: 'numberField',
placeholder: '25',
validations: {
isRequired: true,
minValue: 18,
maxValue: 120,
minValueError: 'You must be at least 18 years old.',
maxValueError: 'Please enter a valid age (18–120).',
},
}minValue / maxValue — checked on submit. A value of 150 with maxValue: 120 shows maxValueError.
float
A decimal number input. Accepts digits and one decimal point. Useful for prices, weights, percentages, coordinates, and any measurement.
{
id: 6,
title: 'Price (₹)',
variableName: 'price',
type: 'float',
placeholder: '999.99',
validations: {
isRequired: true,
minValue: 0,
maxValue: 1000000,
minValueError: 'Price cannot be negative.',
maxValueError: 'Price cannot exceed ₹10,00,000.',
},
}dropdown
A styled single-select dropdown built with Radix UI (fully keyboard-accessible, ARIA-compliant). Best for 3–15 options that fit in a list.
{
id: 7,
title: 'Country',
variableName: 'country',
type: 'dropdown',
placeholder: 'Select a country…',
options: [
{ value: 'in', label: 'India' },
{ value: 'us', label: 'United States' },
{ value: 'gb', label: 'United Kingdom' },
{ value: 'au', label: 'Australia' },
{ value: 'ca', label: 'Canada' },
],
validations: { isRequired: true },
}SelectOption shape:
{
value: string | number; // what gets submitted
label?: string; // what the user sees
title?: string; // fallback if no label
isDisabled?: boolean; // greys out and blocks selection
}Submitted value: the selected value, e.g. 'in'.
reactSelect (searchable single-select)
A searchable dropdown — the user types to filter the list in real time. Ideal for long option lists (states, cities, product codes, 50+ items). Shows a search box inside the dropdown.
{
id: 8,
title: 'Tech Stack',
variableName: 'techStack',
type: 'reactSelect',
placeholder: 'Search or pick one…',
options: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'angular', label: 'Angular' },
{ value: 'svelte', label: 'Svelte' },
{ value: 'solid', label: 'SolidJS' },
{ value: 'next', label: 'Next.js' },
],
validations: { isRequired: true },
}When to use reactSelect vs dropdown:
dropdown→ 3–15 options, simple listreactSelect→ 15+ options, user needs to search
multiSelect
A searchable multi-select field. The user types to filter, then clicks to select. Each selected item appears as a removable chip inside the input. Useful for skills, categories, tags, and permissions.
{
id: 9,
title: 'Skills',
variableName: 'skills',
type: 'multiSelect',
placeholder: 'Add skills…',
options: [
{ value: 'ts', label: 'TypeScript' },
{ value: 'node', label: 'Node.js' },
{ value: 'gql', label: 'GraphQL' },
{ value: 'docker', label: 'Docker' },
{ value: 'k8s', label: 'Kubernetes' },
{ value: 'aws', label: 'AWS' },
],
}Submitted value: an array of selected values — ['ts', 'node', 'docker']
radioButton
A horizontal row of radio buttons. Best for 2–5 mutually exclusive choices where the user needs to see all options at once without opening a dropdown.
{
id: 10,
title: 'Preferred Contact Method',
variableName: 'contactMethod',
type: 'radioButton',
options: [
{ value: 'email', label: 'Email' },
{ value: 'phone', label: 'Phone' },
{ value: 'whatsapp', label: 'WhatsApp' },
],
radioColor: '#6366f1',
validations: {
isRequired: true,
isRequiredError: 'Please select a contact method.',
},
}radioColor — override the accent colour of the selected radio dot. Useful when you need a specific brand colour without changing the whole form theme.
Submitted value: the value of the selected option, e.g. 'email'.
switch / boolean
A toggle switch (on/off). The label sits next to the switch. Ideal for preferences, feature flags, account status.
{
id: 11,
title: 'Receive email notifications',
variableName: 'emailNotifications',
type: 'switch',
isChecked: true, // default state
}
type: 'boolean'is an alias fortype: 'switch'— they are identical.
isChecked — set true to start the switch in the ON state. Default is false.
Submitted value: true or false.
checkbox
A single checkbox. Most commonly used for terms & conditions acceptance, GDPR consent, or any opt-in.
{
id: 12,
title: 'I agree to the Terms & Conditions',
variableName: 'termsAccepted',
type: 'checkbox',
validations: {
isRequired: true,
isRequiredError: 'You must accept the Terms & Conditions to continue.',
},
}Submitted value: true (checked) or false (unchecked).
chipDisplay
A read-only display of chip/tag labels. Use this to show pre-assigned roles, labels, or categories that the user cannot change (e.g. "your assigned permissions are: Editor, Reviewer").
{
id: 13,
title: 'Assigned Roles',
variableName: 'roles',
type: 'chipDisplay',
options: [
{ value: 'editor', label: 'Editor' },
{ value: 'reviewer', label: 'Reviewer' },
{ value: 'viewer', label: 'Viewer' },
],
}The chips are display-only — the user cannot click, remove, or add them.
dateField
A calendar popover for picking a single date. Clicking the field opens a month/year calendar. The user navigates months with arrows and clicks a day. The selected date appears as formatted text in the field.
{
id: 14,
title: 'Date of Birth',
variableName: 'dob',
type: 'dateField',
validations: {
isRequired: true,
maxYearMinus: 0, // cannot be today or in the future
minYearMinus: 100, // cannot be more than 100 years in the past
},
}Submitted value: a date string in DD/MM/YYYY format, e.g. '25/12/1990'.
See the Date validation rules section for the full list of date constraint keys.
dateRange
A dual-calendar date range picker. The user picks a start date and an end date. Both calendars open in a popover side by side.
{
id: 15,
title: 'Project Timeline',
variableName: 'timeline',
type: 'dateRange',
validations: {
isRequired: true,
minDaysPlus: 0, // start date can be today
maxDaysPlus: 365, // end date can be up to 1 year from today
},
}Submitted value:
{ from: '15/06/2025', to: '30/09/2025' }uploadFile
A drag-and-drop file upload zone. Shows a dashed border with an upload icon. After the user selects a file, a preview appears (image thumbnail for images, filename for other types). Supports multi-file uploads.
{
id: 16,
title: 'Supporting Documents',
variableName: 'documents',
type: 'uploadFile',
acceptedExtensions: {
'application/pdf': ['.pdf'],
'image/*': ['.png', '.jpg', '.jpeg'],
},
validations: {
isRequired: true,
maxFileSize: 5 * 1024 * 1024, // 5 MB per file
maxFiles: 3, // allow up to 3 files
minFiles: 1,
},
errorOnFileTooLarge: 'Each file must be under 5 MB.',
errorForInvalidFileType: 'Only PDF, PNG, and JPG files are accepted.',
}Key props for uploadFile:
| Prop | Type | Description |
|---|---|---|
| acceptedExtensions | Record<string, string[]> | MIME type → extensions map. Follows dropzone/MIME format |
| validations.maxFileSize | number | Maximum size in bytes (e.g. 2 * 1024 * 1024 = 2 MB) |
| validations.maxFiles | number | Maximum number of files the user can select |
| validations.minFiles | number | Minimum number of files required |
| validations.exactFiles | number | Require exactly this many files |
| errorOnFileTooLarge | string | Message shown when a file exceeds maxFileSize |
| errorForInvalidFileType | string | Message shown when the file extension is not in acceptedExtensions |
Single image upload — max 5 MB:
{
id: 17,
title: 'Profile Photo',
variableName: 'photo',
type: 'uploadFile',
acceptedExtensions: { 'image/*': ['.png', '.jpg', '.jpeg', '.webp'] },
validations: { maxFileSize: 5 * 1024 * 1024, maxFiles: 1 },
errorOnFileTooLarge: 'Photo must be under 5 MB.',
errorForInvalidFileType: 'Only image files allowed (PNG, JPG, WebP).',
}6. Smart field-type constraints
Setting validations.fieldType on a textField (or any text-based input) automatically applies:
- Character filter — disallowed characters are silently blocked as the user types
- Hard
maxLengthcap — the browser prevents typing beyond this limit - Auto-uppercase — value is uppercased on every keystroke (for code fields like PAN, GST)
- Mobile keyboard hint — the correct keyboard type opens on iOS/Android
You set one property; the library handles everything else. No extra onChange, no extra maxLength, no extra onKeyDown.
All built-in fieldType values
| fieldType | Character filter | Max length | Auto-UPPER | Mobile keyboard |
|---|---|---|---|---|
| mobile | Digits only | 10 | No | Numeric |
| aadhaar | Digits only | 12 | No | Numeric |
| pincode | Digits only | 6 | No | Numeric |
| number | Digits only | — | No | Numeric |
| float | Digits + . only | — | No | Decimal |
| pan | Alphanumeric | 10 | Yes | Text |
| gst | Alphanumeric | 15 | Yes | Text |
| ifsc | Alphanumeric | 11 | Yes | Text |
| rto | Alphanumeric | 11 | Yes | Text |
| chassis | Alphanumeric | 17 | Yes | Text |
| engine | Alphanumeric | 20 | Yes | Text |
| alphanumeric | Alphanumeric | — | No | Text |
| name | Letters + space | — | No | Text |
| firstName | Letters only | — | No | Text |
| middleName | Letters only | — | No | Text |
| lastName | Letters only | — | No | Text |
| email | (no filter) | — | No | Email |
| url | (no filter) | — | No | URL |
| ip | Digits + . + : | — | No | Text |
| regCode | Letters only | 2 (min+max) | Yes | Text |
| regNumber | Digits only | 4 | No | Numeric |
| policyNumber | Alphanumeric | 10 | Yes | Text |
| string | (no filter) | — | No | Text |
| address | (no filter) | — | No | Text |
| password | (no filter) | — | No | Text |
| custom | (no filter) | — | No | Text |
Examples
// Mobile number — digits only, max 10, numeric keyboard, must start with 6–9
{
id: 1,
title: 'Mobile Number',
variableName: 'phone',
type: 'textField',
validations: {
fieldType: 'mobile',
isRequired: true,
errorMessages: { invalid: 'Enter a valid 10-digit Indian mobile number.' },
},
}
// PAN card — alphanumeric only, auto-UPPERCASE, max 10, format ABCDE1234F
{
id: 2,
title: 'PAN Card',
variableName: 'pan',
type: 'textField',
placeholder: 'ABCDE1234F',
validations: {
fieldType: 'pan',
isRequired: true,
errorMessages: { invalid: 'Enter a valid PAN (e.g. ABCDE1234F).' },
},
}
// Vehicle chassis — alphanumeric, auto-UPPERCASE, 5–17 chars, must contain at least one letter
{
id: 3,
title: 'Vehicle Chassis No.',
variableName: 'chassis',
type: 'textField',
placeholder: 'MA3ERLF1S00100001',
validations: {
fieldType: 'chassis',
errorMessages: { invalid: 'Enter a valid chassis number (5–17 alphanumeric characters).' },
},
}
// Registration code — letters only, auto-UPPERCASE, exactly 2 chars (e.g. MH, DL, KA)
{
id: 4,
title: 'Registration Code',
variableName: 'regCode',
type: 'textField',
placeholder: 'MH',
validations: {
fieldType: 'regCode',
errorMessages: { invalid: 'Enter a valid 2-letter state code (e.g. MH, DL, KA).' },
},
}
// Policy number — alphanumeric, must start with P, auto-UPPERCASE, max 10
{
id: 5,
title: 'Policy Number',
variableName: 'policyNumber',
type: 'textField',
placeholder: 'PABC1234',
validations: {
fieldType: 'policyNumber',
errorMessages: { invalid: 'Policy number must start with P and be max 10 characters.' },
},
}7. Override auto-constraints
Auto-constraints are applied by default when you set fieldType. You can override any individual constraint in the validations object without turning off the others.
Available overrides
| Override | Type | Effect |
|---|---|---|
| maxLength: N | number | Change the character cap. 0 = remove the cap entirely |
| allowLowerCase: true | boolean | Allow lowercase input on fields that auto-uppercase (e.g. PAN, GST) |
| isUpperCase: true | boolean | Force uppercase on any field, even those without auto-upper |
| allowAnyChars: true | boolean | Disable the character filter entirely — any character can be typed |
| isNumeric: true | boolean | Force digit-only filter on fields that have no built-in filter |
Examples
// Mobile field with a shorter cap (8 digits instead of 10)
{
validations: {
fieldType: 'mobile',
maxLength: 8, // overrides the default max of 10
},
}
// PAN field — allow lowercase (e.g. for a case-insensitive lookup form)
{
validations: {
fieldType: 'pan',
allowLowerCase: true, // user can type 'abcde1234f', it won't be uppercased
},
}
// GST field — allow any characters (e.g. import/search form that accepts partial input)
{
validations: {
fieldType: 'gst',
allowAnyChars: true, // character filter removed, user can type anything
},
}
// Mobile field with no length cap (for international numbers)
{
validations: {
fieldType: 'mobile',
maxLength: 0, // 0 = no cap; the digits-only filter still applies
},
}
// Plain string field — force uppercase
{
validations: {
fieldType: 'string',
isUpperCase: true,
},
}8. Full ValidationConfig reference
Every field's validations property accepts these keys. All are optional.
interface ValidationConfig {
// ── Required check ────────────────────────────────────────────────────────
isRequired?: boolean; // Show required error if field is empty on submit
isRequiredError?: string; // Custom "required" error message
// ── Smart field-type constraints ──────────────────────────────────────────
fieldType?: string; // One of the 25 built-in type keys (see section 6)
// ── Length ────────────────────────────────────────────────────────────────
minLength?: number; // Minimum character count (soft floor)
maxLength?: number; // Maximum character count (hard cap; 0 = no cap)
minLengthError?: string; // Custom "too short" error message
maxLengthError?: string; // Custom "too long" error message
// ── Numeric value ─────────────────────────────────────────────────────────
minValue?: number; // Minimum numeric value (numberField, float)
maxValue?: number; // Maximum numeric value (numberField, float)
minValueError?: string; // Custom "too small" error message
maxValueError?: string; // Custom "too large" error message
// ── Smart constraint overrides ─────────────────────────────────────────────
isNumeric?: boolean; // Force digit-only filter
isUpperCase?: boolean; // Force uppercase
allowLowerCase?: boolean; // Disable auto-uppercase
allowAnyChars?: boolean; // Disable character filter
// ── Custom regex ──────────────────────────────────────────────────────────
regex?: string; // Custom regex string — overrides fieldType pattern
// ── Date constraints (dateField, dateRange) ───────────────────────────────
// See section 9 for the complete date reference
minDate?: string;
maxDate?: string;
minYearMinus?: number;
maxYearMinus?: number;
minYearPlus?: number;
maxYearPlus?: number;
minDaysPlus?: number;
maxDaysPlus?: number;
minDaysMinus?: number;
maxDaysMinus?: number;
// ── File upload ────────────────────────────────────────────────────────────
maxFileSize?: number; // Max file size in bytes
minFiles?: number; // Minimum number of files
maxFiles?: number; // Maximum number of files
exactFiles?: number; // Require exactly this many files
// ── Custom error messages ──────────────────────────────────────────────────
errorMessages?: {
required?: string; // Overrides isRequiredError
invalid?: string; // Shown when format/pattern check fails
[key: string]: string | undefined;
};
}9. Date validation rules
All date constraints work on both dateField and dateRange. They are evaluated relative to today's date at runtime.
| Key | What it means | Example value | Result |
|---|---|---|---|
| minDaysPlus | Selected date must be at least N days after today | 1 | Tomorrow or later (future booking) |
| maxDaysPlus | Selected date must be at most N days after today | 30 | No more than 30 days in the future |
| minDaysMinus | Selected date must be at most N days before today | 7 | No earlier than 7 days ago |
| maxDaysMinus | Selected date must be at least N days before today | 0 | Any past date (including today) |
| minYearMinus | Selected date must be at most N years before today | 100 | Person must be born within last 100 years |
| maxYearMinus | Selected date must be at least N years before today | 18 | Person must be at least 18 years old |
| minYearPlus | Selected date must be at least N years in the future | 1 | At least 1 year from today |
| maxYearPlus | Selected date must be at most N years in the future | 5 | No more than 5 years ahead |
| minDate | Hard lower bound (ISO string) | '2024-01-01' | Cannot pick before Jan 1 2024 |
| maxDate | Hard upper bound (ISO string) | '2024-12-31' | Cannot pick after Dec 31 2024 |
Common patterns
// Date of birth — at least 18 years old, not more than 100 years ago
validations: { maxYearMinus: 18, minYearMinus: 100 }
// Appointment — tomorrow to 30 days from now
validations: { minDaysPlus: 1, maxDaysPlus: 30 }
// Policy start date — today to 90 days from now
validations: { minDaysPlus: 0, maxDaysPlus: 90 }
// Vehicle purchase date — any past date
validations: { maxDaysMinus: 0 }
// Warranty expiry — at least 1 year in the future
validations: { minYearPlus: 1, maxYearPlus: 10 }10. Sections — grouping fields visually
Instead of a flat fields array, use sections to visually group related fields under labelled headings with a separator line.
import { DynamicForm } from 'quantique-forms';
import type { FormSection } from 'quantique-forms';
const sections: FormSection[] = [
{
id: 'personal',
title: 'Personal Information',
fields: [
{ id: 1, title: 'Full Name', variableName: 'fullName', type: 'textField', validations: { isRequired: true } },
{ id: 2, title: 'Mobile', variableName: 'mobile', type: 'textField', validations: { fieldType: 'mobile' } },
{ id: 3, title: 'Date of Birth', variableName: 'dob', type: 'dateField', validations: { maxYearMinus: 18 } },
],
},
{
id: 'identity',
title: 'Identity Documents',
fields: [
{ id: 4, title: 'PAN Card', variableName: 'pan', type: 'textField', validations: { fieldType: 'pan' } },
{ id: 5, title: 'Aadhaar', variableName: 'aadhaar', type: 'textField', validations: { fieldType: 'aadhaar' } },
],
},
{
id: 'vehicle',
title: 'Vehicle Details',
fields: [
{ id: 6, title: 'Chassis No.', variableName: 'chassis', type: 'textField', validations: { fieldType: 'chassis' } },
{ id: 7, title: 'Engine No.', variableName: 'engine', type: 'textField', validations: { fieldType: 'engine' } },
],
},
];
export default function MultiSectionForm() {
return (
<DynamicForm
sections={sections}
onSubmit={(data) => console.log(data)}
renderActions={(isSubmitting) => (
<button type="submit" disabled={isSubmitting}>Submit</button>
)}
/>
);
}What the user sees: Each section renders with its title as a heading and a visual separator between sections. All fields still validate together on submit.
Use either
fieldsORsections— not both. If you passsections, the top-levelfieldsprop is ignored.
11. Theming with CSS variables
Pass a theme object to DynamicForm to customise the colour palette, border radius, and other visual properties. Changes apply to all field types in the form simultaneously.
<DynamicForm
fields={fields}
onSubmit={handleSubmit}
renderActions={...}
theme={{
brand: '#6366f1', // Primary colour: buttons, active borders, selected states
brandHover: '#4f46e5', // Hover state for brand colour
brandLight: '#e0e7ff', // Light tint of brand: chip backgrounds, subtle highlights
brandMuted: '#eef2ff', // Very light tint: chip hover backgrounds
ink: '#111827', // Primary text colour
inkMuted: '#6b7280', // Secondary text: placeholders, hints
inkLight: '#9ca3af', // Tertiary text: disabled labels
inkDark: '#030712', // Extra-dark text: strong headings
surface: '#ffffff', // Input background
surfaceMuted:'#f9fafb', // Disabled input background
borderDefault:'#e5e7eb', // Input border (default/resting)
borderMuted: '#f3f4f6', // Subtle borders inside components
danger: '#ef4444', // Error colour: error borders, error messages
dangerLight: '#fee2e2', // Error background tint
success: '#22c55e', // Success colour (future use)
radius: '0.5rem', // Border radius for inputs, dropdowns, buttons
}}
/>Theme presets
Indigo (default)
theme={{ brand: '#6366f1', brandHover: '#4f46e5', danger: '#ef4444', radius: '0.5rem' }}Violet
theme={{ brand: '#8b5cf6', brandHover: '#7c3aed', danger: '#f43f5e', radius: '0.75rem' }}Amber/orange
theme={{ brand: '#f59e0b', brandHover: '#d97706', danger: '#f43f5e', radius: '0.5rem' }}Green/emerald
theme={{ brand: '#10b981', brandHover: '#059669', danger: '#ef4444', radius: '0.375rem' }}Sharp (no rounding)
theme={{ brand: '#1d4ed8', radius: '0' }}Behind the scenes
The theme prop sets CSS custom properties (--qf-brand, --qf-danger, --qf-radius, etc.) on a wrapping div. All form components read from these variables. This means:
- You can also set them globally in your CSS for a persistent theme
- You can have different themes on different forms on the same page
- The theme has no global side effects outside the form wrapper
Full list of CSS variables:
--qf-brand
--qf-brand-hover
--qf-brand-light
--qf-brand-muted
--qf-ink
--qf-ink-muted
--qf-ink-light
--qf-ink-dark
--qf-surface
--qf-surface-muted
--qf-border-default
--qf-border-muted
--qf-danger
--qf-danger-light
--qf-success
--qf-radius12. Inter-field reactivity
The onFieldChange callback fires every time any field value changes. Use it to show/hide fields, pre-fill values, trigger API calls, or sync one field's value to another.
<DynamicForm
fields={fields}
onSubmit={handleSubmit}
renderActions={...}
onFieldChange={(name, value, allValues, setValue) => {
// name — the variableName of the field that changed
// value — the new value of that field
// allValues — a snapshot of every field's current value
// setValue — react-hook-form's setValue — use this to programmatically set any field
if (name === 'country' && value === 'in') {
// Auto-fill currency when country is India
setValue('currency', 'INR');
}
if (name === 'vehicleType' && value === 'electric') {
// Clear engine number for electric vehicles
setValue('engineNo', '');
}
}}
/>Practical examples
Auto-fill city from pincode:
onFieldChange={async (name, value, _all, setValue) => {
if (name === 'pincode' && String(value).length === 6) {
const city = await fetchCityFromPincode(String(value));
if (city) setValue('city', city);
}
}}Cascade dropdown — state → city:
onFieldChange={(name, value, _all, setValue) => {
if (name === 'state') {
// Reset city when state changes
setValue('city', '');
// You can also update the city field's options via your state management
}
}}Log all changes for debugging:
onFieldChange={(name, value) => {
console.log(`${name} →`, value);
}}13. Pre-filling default values
Pass a defaultValues object to pre-populate the form. Keys must match each field's variableName.
<DynamicForm
fields={fields}
onSubmit={handleSubmit}
renderActions={...}
defaultValues={{
fullName: 'Jane Smith',
email: '[email protected]',
country: 'in',
isActive: true,
dob: '01/01/1990',
}}
/>When to use:
- Edit forms (pre-fill from an existing database record)
- Wizard-style multi-step forms (carry values from step 1 to step 2)
- Forms with smart defaults (e.g. today's date, the user's own country)
14. Custom submit / action buttons
The renderActions prop gives you full control over the submit/reset/cancel buttons. The function receives isSubmitting (boolean) so you can show a loading state.
Submit + Reset:
renderActions={(isSubmitting) => (
<div style={{ display: 'flex', gap: 12 }}>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving…' : 'Save changes'}
</button>
<button type="reset">
Reset
</button>
</div>
)}Save draft + Publish:
renderActions={(isSubmitting) => (
<div style={{ display: 'flex', gap: 12 }}>
<button type="submit" disabled={isSubmitting}>
Publish
</button>
<button type="button" onClick={handleSaveDraft}>
Save draft
</button>
<button type="button" onClick={handleCancel}>
Cancel
</button>
</div>
)}
type="reset"clears the form.type="submit"triggers validation andonSubmit.type="button"does neither — useful for secondary actions like "save draft" or "go back".
15. DynamicForm prop reference
| Prop | Type | Required | Description |
|---|---|---|---|
| fields | FieldConfig[] | No* | Flat array of field configs |
| sections | FormSection[] | No* | Array of section objects — each section has a title and fields array |
| onSubmit | (data: Record<string, unknown>) => void \| Promise<void> | Yes | Called with the form data when all fields pass validation |
| renderActions | (isSubmitting: boolean) => ReactNode | No | Render your own submit/reset buttons |
| defaultValues | Record<string, unknown> | No | Pre-populate field values by variableName |
| onFieldChange | (name, value, allValues, setValue) => void | No | Called on every field value change |
| theme | FormTheme | No | Colour/radius overrides for the form |
| className | string | No | Additional CSS class on the form wrapper |
*Either
fieldsorsectionsmust be provided, but not both.
16. FieldConfig full reference
| Key | Type | Required | Description |
|---|---|---|---|
| id | number \| string | Yes | Unique identifier within the form |
| title | string | Yes | Label shown above the field |
| variableName | string | Yes | Key in the submitted data object |
| type | FieldType | Yes | Which field component to render (see section 5) |
| placeholder | string | No | Placeholder text inside the input |
| info | string | No | Helper text shown below the label |
| note | string | No | Additional note shown below the field |
| options | SelectOption[] | For choice types | Options for dropdown, radioButton, multiSelect, etc. |
| validations | ValidationConfig | No | All validation rules and constraints |
| isDisabled | boolean | No | Disable the field (greyed out, not editable) |
| isChecked | boolean | No | Default ON state for switch / boolean |
| showTime | boolean | No | Show time picker alongside calendar in dateField |
| rowSize | number | No | Number of text rows for multiTextField |
| radioColor | string | No | Custom colour for radioButton selected dot |
| acceptedExtensions | Record<string, string[]> | No | MIME type → extension map for uploadFile |
| errorOnFileTooLarge | string | No | Error message for oversized files in uploadFile |
| errorForInvalidFileType | string | No | Error message for wrong file type in uploadFile |
| containerClass | string | No | CSS class on the field's outer container |
| value | unknown | No | Static/override value (rarely needed — use defaultValues instead) |
17. TypeScript types
All types are exported directly from 'quantique-forms':
import type {
FieldConfig, // Single field definition
FieldType, // Union of all 14 type string literals
FormSection, // { id, title?, fields: FieldConfig[] }
SelectOption, // { value, label?, title?, isDisabled? }
ValidationConfig, // All validation rules
FormTheme, // All CSS variable theme keys
DynamicFormProps, // Props for the DynamicForm component
} from 'quantique-forms';18. Real-world example — vehicle registration form
A complete vehicle insurance registration form using sections, smart field-type constraints, date rules, and file upload:
import 'quantique-forms/styles';
import { DynamicForm } from 'quantique-forms';
import type { FormSection } from 'quantique-forms';
const sections: FormSection[] = [
{
id: 'owner',
title: 'Owner Details',
fields: [
{
id: 1,
title: 'Full Name',
variableName: 'ownerName',
type: 'textField',
placeholder: 'Rajesh Kumar',
validations: { isRequired: true, fieldType: 'name' },
},
{
id: 2,
title: 'Mobile Number',
variableName: 'mobile',
type: 'textField',
placeholder: '9876543210',
validations: {
isRequired: true,
fieldType: 'mobile',
errorMessages: { invalid: 'Enter a valid 10-digit Indian mobile number.' },
},
},
{
id: 3,
title: 'PAN Card',
variableName: 'pan',
type: 'textField',
placeholder: 'ABCDE1234F',
validations: {
isRequired: true,
fieldType: 'pan',
errorMessages: { invalid: 'Enter a valid PAN (e.g. ABCDE1234F).' },
},
},
{
id: 4,
title: 'Aadhaar Number',
variableName: 'aadhaar',
type: 'textField',
placeholder: '234567890123',
validations: {
isRequired: true,
fieldType: 'aadhaar',
errorMessages: { invalid: 'Enter a valid 12-digit Aadhaar number.' },
},
},
{
id: 5,
title: 'Date of Birth',
variableName: 'dob',
type: 'dateField',
validations: {
isRequired: true,
maxYearMinus: 18, // must be at least 18 years old
minYearMinus: 100,
},
},
],
},
{
id: 'vehicle',
title: 'Vehicle Details',
fields: [
{
id: 6,
title: 'Registration Code',
variableName: 'regCode',
type: 'textField',
placeholder: 'MH',
info: 'State/UT code (e.g. MH, DL, KA)',
validations: {
isRequired: true,
fieldType: 'regCode',
errorMessages: { invalid: 'Enter a valid 2-letter state code.' },
},
},
{
id: 7,
title: 'Registration Number',
variableName: 'regNumber',
type: 'textField',
placeholder: '01',
info: '1–4 digit district code',
validations: {
isRequired: true,
fieldType: 'regNumber',
errorMessages: { invalid: 'Enter a valid registration number (1–4 digits).' },
},
},
{
id: 8,
title: 'Chassis Number',
variableName: 'chassis',
type: 'textField',
placeholder: 'MA3ERLF1S00100001',
validations: {
isRequired: true,
fieldType: 'chassis',
errorMessages: { invalid: 'Enter a valid chassis number (5–17 characters).' },
},
},
{
id: 9,
title: 'Engine Number',
variableName: 'engine',
type: 'textField',
placeholder: 'G16B12345',
validations: {
isRequired: true,
fieldType: 'engine',
errorMessages: { invalid: 'Enter a valid engine number (must contain at least one letter).' },
},
},
{
id: 10,
title: 'Vehicle Purchase Date',
variableName: 'purchaseDate',
type: 'dateField',
validations: {
isRequired: true,
maxDaysMinus: 0, // must be in the past
},
},
],
},
{
id: 'insurance',
title: 'Insurance Details',
fields: [
{
id: 11,
title: 'Policy Number',
variableName: 'policyNumber',
type: 'textField',
placeholder: 'PABC1234',
validations: {
isRequired: true,
fieldType: 'policyNumber',
errorMessages: { invalid: 'Policy number must start with P, max 10 characters.' },
},
},
{
id: 12,
title: 'Policy Expiry Date',
variableName: 'policyExpiry',
type: 'dateField',
validations: {
isRequired: true,
minDaysPlus: 1, // must be a future date
},
},
{
id: 13,
title: 'Insurance Document',
variableName: 'insuranceDoc',
type: 'uploadFile',
acceptedExtensions: { 'application/pdf': ['.pdf'], 'image/*': ['.jpg', '.jpeg', '.png'] },
validations: {
isRequired: true,
maxFileSize: 5 * 1024 * 1024,
maxFiles: 1,
},
errorOnFileTooLarge: 'Document must be under 5 MB.',
errorForInvalidFileType: 'Only PDF, JPG, or PNG accepted.',
},
],
},
{
id: 'consent',
title: 'Consent',
fields: [
{
id: 14,
title: 'I confirm that all details are correct and I accept the Terms & Conditions.',
variableName: 'termsAccepted',
type: 'checkbox',
validations: {
isRequired: true,
isRequiredError: 'Please confirm and accept the terms to submit.',
},
},
],
},
];
export default function VehicleRegistrationForm() {
const handleSubmit = (data: Record<string, unknown>) => {
console.log('Form submitted:', data);
// data.ownerName, data.pan, data.chassis, data.policyNumber, etc.
};
return (
<DynamicForm
sections={sections}
onSubmit={handleSubmit}
theme={{
brand: '#1d4ed8',
brandHover: '#1e40af',
danger: '#dc2626',
radius: '0.5rem',
}}
renderActions={(isSubmitting) => (
<div style={{ display: 'flex', gap: 12 }}>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Registering…' : 'Register Vehicle'}
</button>
<button type="reset">Clear form</button>
</div>
)}
/>
);
}What this form validates automatically:
regCode— exactly 2 uppercase letters (MH, DL, KA, etc.) — digits/lowercase blocked at keystrokechassis— 5–17 uppercase alphanumeric, must contain at least one letterengine— 2–25 alphanumeric, must contain at least one letter (all-digit strings rejected)policyNumber— starts with P, max 10 uppercase alphanumeric charactersdob— person must be at least 18 years oldpurchaseDate— must be in the pastpolicyExpiry— must be in the futurepan— ABCDE1234F format (5 letters + 4 digits + 1 letter)aadhaar— 12 digits, first digit 2–9mobile— 10 digits, starts with 6–9
19. Changelog
v1.0.5
- Added
regCodesmart constraint: letters-only filter, auto-uppercase, min+max = 2 characters (e.g.MH,DL) - Added
regNumbersmart constraint: digits-only filter, max 4 characters, numeric keyboard - Added
policyNumbersmart constraint: alphanumeric filter, auto-uppercase, max 10 characters - Added
minLengthtoFieldTypeConstraintsandresolveFieldTypeConstraints—regCodeuses this to enforce exactly 2 characters - Fixed
chassisandenginevalidators now require at least one letter — all-digit strings (e.g.000000000000) are correctly rejected - Fixed
quantique-field-validatordependency changed toworkspace:*in monorepo, externalized from vite build to prevent CJS/ESM bundling conflicts
v1.0.4
- Initial public release
- 14 field types: textField, email, password, multiTextField, numberField, float, dropdown, reactSelect, multiSelect, radioButton, switch/boolean, checkbox, chipDisplay, dateField, dateRange, uploadFile
- Smart field-type constraints for 24 fieldType values
- CSS variable theming via
--qf-* - Sections support
- Inter-field reactivity via
onFieldChange - Powered by
react-hook-form+quantique-field-validator
License
MIT © Saket Brij Sinha
