npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

Readme

quantique-forms

npm version license react typescript validator

JSON-driven dynamic form engine for React.
Describe your entire form as a plain JavaScript/TypeScript object. DynamicForm renders 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

  1. What is this?
  2. Why use it?
  3. Installation
  4. Quick start — 60 seconds
  5. All 14 field types with examples
  6. Smart field-type constraints
  7. Override auto-constraints
  8. Full ValidationConfig reference
  9. Date validation rules
  10. Sections — grouping fields visually
  11. Theming with CSS variables
  12. Inter-field reactivity
  13. Pre-filling default values
  14. Custom submit / action buttons
  15. DynamicForm prop reference
  16. FieldConfig full reference
  17. TypeScript types
  18. Real-world example — vehicle registration form
  19. 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-forms

Peer dependencies (install if not already in your project):

npm install react react-dom

react-hook-form and quantique-field-validator ship as dependencies inside quantique-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
  • onSubmit is only called when every field passes validation
  • The submitted data object keys match each field's variableName

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 list
  • reactSelect → 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 for type: '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 maxLength cap — 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 fields OR sections — not both. If you pass sections, the top-level fields prop 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-radius

12. 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 and onSubmit. 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 fields or sections must 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 keystroke
  • chassis — 5–17 uppercase alphanumeric, must contain at least one letter
  • engine — 2–25 alphanumeric, must contain at least one letter (all-digit strings rejected)
  • policyNumber — starts with P, max 10 uppercase alphanumeric characters
  • dob — person must be at least 18 years old
  • purchaseDate — must be in the past
  • policyExpiry — must be in the future
  • pan — ABCDE1234F format (5 letters + 4 digits + 1 letter)
  • aadhaar — 12 digits, first digit 2–9
  • mobile — 10 digits, starts with 6–9

19. Changelog

v1.0.5

  • Added regCode smart constraint: letters-only filter, auto-uppercase, min+max = 2 characters (e.g. MH, DL)
  • Added regNumber smart constraint: digits-only filter, max 4 characters, numeric keyboard
  • Added policyNumber smart constraint: alphanumeric filter, auto-uppercase, max 10 characters
  • Added minLength to FieldTypeConstraints and resolveFieldTypeConstraintsregCode uses this to enforce exactly 2 characters
  • Fixed chassis and engine validators now require at least one letter — all-digit strings (e.g. 000000000000) are correctly rejected
  • Fixed quantique-field-validator dependency changed to workspace:* 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