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

forme-engine

v1.0.6

Published

React form generation library — conditional display, multi-step pagination, OTP, file upload, repeat groups

Downloads

142

Readme

forme-engine

A powerful, JSON-driven React form engine — conditional logic, multi-step pagination, OTP, file uploads, repeat groups, multilingual labels, and more. Build complex forms without writing form code.

npm version license TypeScript React


Why forme-engine?

Most React form libraries give you primitives — you still wire validation, conditionals, multi-step logic, file uploads, and OTP flows yourself. forme-engine is the only library in the React ecosystem that combines all of the following out of the box, with zero boilerplate:

| Feature | react-hook-form | Formik | RJSF | form-engine | |---|:---:|:---:|:---:|:---:| | JSON-driven rendering | ❌ | ❌ | ✅ | ✅ | | Conditional display (AND/OR/computed) | ❌ | ❌ | ⚠️ | ✅ | | Multi-step + progress bar | ❌ | ❌ | ❌ | ✅ | | Repeat groups (dynamic arrays) | ❌ | ❌ | ✅ | ✅ | | OTP flow (send + verify + resend) | ❌ | ❌ | ❌ | ✅ | | File-to-bucket upload/delete | ❌ | ❌ | ❌ | ✅ | | Cascading dropdowns | ❌ | ❌ | ❌ | ✅ | | Multilingual labels | ❌ | ❌ | ⚠️ | ✅ | | Radix UI + Tailwind (shadcn-compatible) | ❌ | ❌ | ❌ | ✅ | | Zod validation (auto-generated) | ❌ | ❌ | ❌ | ✅ | | Draft persistence (Zustand) | ❌ | ❌ | ❌ | ✅ |

The sweet spot: if your stack is React + Zod + Radix UI + Tailwind (the modern default), forme-engine plugs in natively and eliminates weeks of form infrastructure work.


Overview

forme-engine lets you describe a form as a plain JSON object and render it as a fully functional, validated, multi-step React form. No boilerplate. No manual useState for every field. You define the structure — the engine handles the rest.

Key capabilities:

  • All standard field types — text, textarea, dropdown, radio, checkbox, date, number, boolean, password, file
  • Rich media fields — image, video, audio, document, PDF, voice recording
  • Rich text editor — TipTap integration
  • OTP verification — built-in phone/email OTP flow with resend logic
  • File-to-bucket uploads — upload and delete files via a configurable API
  • Conditional display — show/hide sections and fields based on other field values
  • Repeat groups — dynamic lists of fields (e.g. list of passengers, children, etc.)
  • Multi-step pagination — by section or by field count, with progress bar
  • Multilingual labels — every text supports { fr: "...", en: "..." } objects
  • Zod validation — automatic schema generation from field definitions
  • Form persistence — draft saving via Zustand
  • Theming — primary color + per-container style overrides

Installation

npm install forme-engine

Peer dependencies

The following packages must be installed in your project:

npm install react react-dom react-hook-form zod zustand lucide-react react-hot-toast \
  @tiptap/react @tiptap/starter-kit @tiptap/extension-image \
  @tiptap/extension-link @tiptap/extension-placeholder

Quick start

import { FormEngine } from "forme-engine";
import type { FormStructure } from "forme-engine";

const form: FormStructure = {
  name: "Contact",
  provider: "Acme Corp",
  description: "Simple contact form",
  status: "ACTIVE",
  isDeclaration: 0,
  isTest: 0,
  theme: { primaryColor: "#008080" },
  layoutOptions: {
    paginationMode: "bySection",
    progressBarType: { type: "linear", visible: true },
  },
  sections: [
    {
      sectionId: "section-contact",
      title: { fr: "Vos informations", en: "Your information" },
      formFields: [
        {
          formFieldId: "f-name",
          fieldName: "full_name",
          fieldType: "text",
          label: { fr: "Nom complet", en: "Full name" },
          response: { responseValue: "" },
          width: "half",
          validations: [
            { validationType: "required", errMsg: "Le nom est requis" },
          ],
        },
        {
          formFieldId: "f-email",
          fieldName: "email",
          fieldType: "text",
          label: "Email",
          response: { responseValue: "" },
          width: "half",
          validations: [
            { validationType: "required", errMsg: "L'email est requis" },
            { validationType: "email", errMsg: "Email invalide" },
          ],
        },
      ],
    },
  ],
};

export default function App() {
  return (
    <FormEngine
      form={form}
      formId="contact-form"
      onSubmit={(data) => console.log(data)}
      currentLang="fr"
      submitButtonText="Envoyer"
    />
  );
}

<FormEngine> props

| Prop | Type | Required | Default | Description | |------|------|:--------:|---------|-------------| | form | FormStructure | ✅ | — | The complete form definition object | | formId | string | ✅ | — | Unique instance ID — used for draft persistence | | onSubmit | (data: Record<string, any>) => void | ✅ | — | Called when the form is valid and submitted | | currentLang | string | | "fr" | Active language code for multilingual labels | | submitButtonText | string | | "Continuer" | Label of the submit / next button | | paginationMode | "byFields" \| "bySection" | | "byFields" | Overrides form.layoutOptions.paginationMode |


Form structure reference

FormStructure

The root object passed to the form prop.

{
  name: string;
  provider: string;
  description: string;
  status: "ACTIVE" | "INACTIVE";
  isDeclaration: number;   // 1 = official declaration, 0 = standard
  isTest: number;          // 1 = test, 0 = production
  version?: string;
  createdAt?: string;      // ISO date string
  updatedAt?: string;      // ISO date string
  sections: Section[];
  theme?: FormTheme;
  layoutOptions?: FormEngineLayoutOptions;
}

FormTheme

{
  primaryColor?: string;  // Hex color — e.g. "#008080"
}

FormEngineLayoutOptions

{
  paginationMode?: "byFields" | "bySection";
  progressBarType?: ProgressBarType;
  headerOptions?: HeaderOptions;
  displayDeleteButton?: boolean;       // Show "clear all" button — default: true
  containerStyles?: ContainerStylesOptions;
}

ProgressBarType

| type value | Description | |---|---| | "default_step" | Classic numbered steps | | "linear" | Horizontal progress bar | | "pastel" | Pastel-toned step bar | | "custom" | Custom colors via colors | | "section_bubble" | Bubble indicators per section | | "section_bubble_pastel" | Pastel bubble indicators |

progressBarType: {
  type: "linear",
  visible: true,
  colors: {
    background: "#e5e7eb",
    foreground: "#008080",
  },
}

HeaderOptions

{
  visible?: boolean;
  type: "default" | "custom";
  customHeader?: React.ReactNode;  // Only used when type === "custom"
}

ContainerStylesOptions

Customize the border and background of the form containers:

{
  mainContainer?: ContainerStyles;    // Outer container (sections + buttons)
  sectionContainer?: ContainerStyles; // Inner container (each section)
}

// ContainerStyles
{
  borderColor?: string;         // e.g. "#e5e7eb"
  borderWidth?: string | number; // e.g. "1px" or 1
  backgroundColor?: string;     // e.g. "#f9fafb"
}

Section

A section groups related fields. Each section becomes a page in "bySection" mode.

{
  sectionId: string;
  title?: string | { [lang: string]: string } | null;
  subTitle?: string | { [lang: string]: string } | null;
  isSpacer?: string;
  position?: number;
  formFields: FormField[];
  conditionalDisplay?: ConditionalDisplayGroup;
}

FormField

The core building block. Every field in section.formFields follows this shape:

{
  formFieldId: string;           // Unique ID — UUID recommended
  fieldName: string;             // Technical key — becomes the key in onSubmit data
  slug?: string;                 // Human-readable key for URLs / exports
  fieldType: FieldType;          // See table below
  label: string | { [lang: string]: string };
  response: { responseValue: string | string[] | number | number[] | boolean | File | File[] | null };
  selectOptions?: SelectOption[];
  validations?: ValidationRule[];
  position?: number;
  placeholder?: string | { [lang: string]: string };
  tooltip?: string | { [lang: string]: string };
  helpText?: string | { [lang: string]: string };
  example?: string | { [lang: string]: string };
  width?: "full" | "half";
  enableSearch?: boolean;
  conditionalDisplay?: ConditionalDisplayGroup;
  repeatGroup?: FormRepeatGroup;
  repeatRule?: RepeatRule;
  fileToBucketManage?: FileToBucketManage;
  otpFormFieldExecOptions?: OTPFormFieldExecOptions;
  emailFormFieldExecOptions?: EmailFormFieldExecOptions;
  detailInfos?: DetailInfosConfig;
  dynamicFilterRule?: DynamicFilterRule;
  flattenOnSubmit?: boolean;
}

Field types

| fieldType | UI rendered | Notes | |---|---|---| | "text" | Text input | Also used for email with validation | | "textarea" | Multi-line input | | | "dropdown" | Select list | Requires selectOptions. Supports enableSearch | | "radio" | Radio buttons | Requires selectOptions | | "checkbox" | Checkboxes | Requires selectOptions | | "date" | Date picker | | | "number" | Numeric input | | | "bool" | Toggle / switch | | | "file" | File upload | Generic | | "password" | Password input | Masked | | "IMAGE" | Image upload | Preview included | | "VIDEO" | Video upload | | | "AUDIO" | Audio upload | | | "DOCUMENT" | Document upload | | | "PDF" | PDF upload | | | "VOICE" | Voice recording | Browser mic | | "TIP_TAP_DOC_TEXT" | Rich text editor | TipTap-powered | | "OTP" | OTP code input | Requires otpFormFieldExecOptions |


SelectOption

Used with dropdown, radio, and checkbox fields:

{
  value: string;
  label: string | { [lang: string]: string };
  points?: number;  // Score / weight for quiz-like forms
  icon?: string;    // Icon name or URL
}

ValidationRule

{
  validationType?: ValidationType;
  value?: number | string | string[];
  errMsg: string;  // Error message displayed to the user
}

Validation types

| validationType | value type | Description | |---|---|---| | "required" | — | Field must not be empty | | "minLength" | number | Minimum character count | | "maxLength" | number | Maximum character count | | "regex" | string | Must match the given pattern | | "email" | — | Must be a valid email address | | "number" | — | Must be a valid number | | "fileSize" | number | Max file size in bytes | | "phone_number" | — | Must be a valid phone number | | "fileType" | string[] | Allowed MIME types — e.g. ["image/png", "image/jpeg"] |


Conditional display

Show or hide a section or field based on the values of other fields.

{
  rules: ConditionalDisplayRule | ConditionalDisplayRule[];
  logic?: "AND" | "OR";   // Default: "AND"
}

ConditionalDisplayRule

{
  fieldName?: string | string[];       // Target by technical key
  formFieldId?: string | string[];     // Target by UUID
  operator?: ConditionalOperator;
  value?: string | number | boolean | string[] | number[];
  numeric_compare_to?: number;         // For aggregation comparisons
  typeComputation?: "SOMME" | "MULTIPLICATION";
}

Operators

"==" · "!=" · ">" · "<" · "IN" · "NOT IN" · "CONTAINS" · "NOT CONTAINS"

Examples

// Show field only if "has_vehicle" equals "yes"
conditionalDisplay: {
  rules: { fieldName: "has_vehicle", operator: "==", value: "yes" },
}

// Show section if country is France OR Belgium
conditionalDisplay: {
  rules: { fieldName: "country", operator: "IN", value: ["FR", "BE"] },
}

// Show if score1 + score2 > 10
conditionalDisplay: {
  rules: {
    fieldName: ["score1", "score2"],
    typeComputation: "SOMME",
    operator: ">",
    numeric_compare_to: 10,
  },
}

Repeat groups

A FormRepeatGroup defines a block of fields that users can repeat N times (e.g. list of children, co-applicants).

// On FormField
repeatGroup: {
  repeatGroupId: string;
  fieldName: string;
  label: string | { [lang: string]: string };
  minRepeats?: number;
  maxRepeats?: number;
  initialRepeats?: number;
  formFields: FormField[];
  position?: number;
  conditionalDisplay?: ConditionalDisplayGroup;
}

repeatRule: {
  min?: number;
  max?: number;
  prefillEmpty?: boolean;   // Pre-fill one empty instance on initial render
}

OTP field

{
  formFieldId: "f-otp",
  fieldName: "otp_code",
  fieldType: "OTP",
  label: "Code de vérification",
  response: { responseValue: "" },
  otpFormFieldExecOptions: {
    serverDns: "https://api.example.com",
    postApiEndPoint: "/api/otp/verify",
    otpLength: 6,
    linkedEmailFieldName: "email",   // Triggers OTP send when this field is filled
    sendOTPApiConfig: {
      serverDns: "https://api.example.com",
      postApiEndPoint: "/api/otp/send",
      bearer: "my-token",
    },
    autoValidate: true,
    enableResend: true,
    resendCooldownSeconds: 60,
  },
}

To trigger OTP send automatically from the email field, add emailFormFieldExecOptions to it:

{
  formFieldId: "f-email",
  fieldName: "email",
  fieldType: "text",
  label: "Email",
  response: { responseValue: "" },
  validations: [{ validationType: "email", errMsg: "Email invalide" }],
  emailFormFieldExecOptions: {
    triggerOTPSend: true,
    linkedOTPFieldName: "otp_code",
    otpSendApiConfig: {
      serverDns: "https://api.example.com",
      postApiEndPoint: "/api/otp/send",
    },
  },
}

File uploads to a bucket

Add fileToBucketManage to any file-type field to automatically upload on selection and delete on removal:

fileToBucketManage: {
  uploadOption: {
    serverDns: "https://bucket.example.com",
    postApiEndPoint: "/api/files/upload-file",
    payload: { secret_key: "my-secret", folder_name: "avatars" },
    bearer: "my-token",
    timeoutMs: 30000,
  },
  deleteOption: {
    serverDns: "https://bucket.example.com",
    deleteApiEndPoint: "/api/files/delete-file",
    payload: { secret_key: "my-secret", folder_name: "avatars" },
  },
}

Dynamic filter (cascading dropdowns)

Filter a dropdown's options based on another field's selected value:

{
  formFieldId: "f-city",
  fieldName: "city",
  fieldType: "dropdown",
  label: "Ville",
  response: { responseValue: "" },
  dynamicFilterRule: {
    dependentFieldName: "country",   // When "country" changes, reload city options
    filterType: "exact",
    dataSource: {
      type: "static",
      data: {
        FR: [
          { value: "paris", label: "Paris" },
          { value: "lyon", label: "Lyon" },
        ],
        BE: [
          { value: "bruxelles", label: "Bruxelles" },
          { value: "liege", label: "Liège" },
        ],
      },
    },
  },
}

Detail fields on selection

Automatically reveal additional fields when the user selects a specific option:

{
  formFieldId: "f-sector",
  fieldName: "sector",
  fieldType: "dropdown",
  label: "Secteur",
  response: { responseValue: "" },
  selectOptions: [
    { value: "agri", label: "Agriculture" },
    { value: "other", label: "Autre" },
  ],
  detailInfos: {
    other: {
      label: "Précisez votre secteur",
      formFields: [
        {
          formFieldId: "f-sector-detail",
          fieldName: "sector_detail",
          fieldType: "text",
          label: "Secteur exact",
          response: { responseValue: "" },
        },
      ],
    },
  },
}

Multilingual support

Every label, placeholder, tooltip, helpText, and example field accepts either a plain string or a language map:

label: { fr: "Nom complet", en: "Full name", es: "Nombre completo" }

Pass currentLang="en" to <FormEngine> to switch the active language at runtime.


What onSubmit receives

The onSubmit callback receives a flat Record<string, any> where each key is a field's fieldName. File fields return the uploaded URL (if fileToBucketManage is configured) or the File object. Repeat groups return an array of objects.

// Example output
{
  full_name: "Alice Dupont",
  email: "[email protected]",
  sector: "other",
  sector_detail: "Fintech",
  passengers: [
    { passenger_name: "Bob", passenger_age: 32 },
    { passenger_name: "Carol", passenger_age: 28 },
  ],
  id_document: "https://bucket.example.com/files/doc-abc123.pdf",
}

Complete real-world example

A 3-step registration form with:

  • Step 1 — identity (text + email + OTP verification)
  • Step 2 — profile (dropdown + conditional field + cascading dropdown)
  • Step 3 — passengers (repeat group, 1–5 entries)
import { FormEngine } from "forme-engine";
import type { FormStructure } from "forme-engine";

const registrationForm: FormStructure = {
  name: "Registration",
  provider: "Acme Corp",
  description: "Multi-step registration with OTP and repeat group",
  status: "ACTIVE",
  isDeclaration: 0,
  isTest: 0,
  theme: { primaryColor: "#6366f1" },
  layoutOptions: {
    paginationMode: "bySection",
    progressBarType: { type: "linear", visible: true },
    displayDeleteButton: false,
  },
  sections: [
    // ── Step 1 — Identity + OTP ──────────────────────────────────────
    {
      sectionId: "s-identity",
      title: { fr: "Votre identité", en: "Your identity" },
      formFields: [
        {
          formFieldId: "f-firstname",
          fieldName: "first_name",
          fieldType: "text",
          label: { fr: "Prénom", en: "First name" },
          response: { responseValue: "" },
          width: "half",
          validations: [{ validationType: "required", errMsg: "Requis" }],
        },
        {
          formFieldId: "f-lastname",
          fieldName: "last_name",
          fieldType: "text",
          label: { fr: "Nom", en: "Last name" },
          response: { responseValue: "" },
          width: "half",
          validations: [{ validationType: "required", errMsg: "Requis" }],
        },
        {
          formFieldId: "f-email",
          fieldName: "email",
          fieldType: "text",
          label: "Email",
          response: { responseValue: "" },
          validations: [
            { validationType: "required", errMsg: "Requis" },
            { validationType: "email", errMsg: "Email invalide" },
          ],
          // Triggers OTP send automatically when a valid email is entered
          emailFormFieldExecOptions: {
            triggerOTPSend: true,
            linkedOTPFieldName: "otp_code",
            otpSendApiConfig: {
              serverDns: "https://api.example.com",
              postApiEndPoint: "/api/otp/send",
            },
          },
        },
        {
          formFieldId: "f-otp",
          fieldName: "otp_code",
          fieldType: "OTP",
          label: { fr: "Code de vérification (6 chiffres)", en: "Verification code (6 digits)" },
          response: { responseValue: "" },
          otpFormFieldExecOptions: {
            serverDns: "https://api.example.com",
            postApiEndPoint: "/api/otp/verify",
            otpLength: 6,
            linkedEmailFieldName: "email",
            autoValidate: true,
            enableResend: true,
            resendCooldownSeconds: 60,
          },
        },
      ],
    },

    // ── Step 2 — Profile (conditional + cascading) ───────────────────
    {
      sectionId: "s-profile",
      title: { fr: "Votre profil", en: "Your profile" },
      formFields: [
        {
          formFieldId: "f-country",
          fieldName: "country",
          fieldType: "dropdown",
          label: { fr: "Pays", en: "Country" },
          response: { responseValue: "" },
          enableSearch: true,
          selectOptions: [
            { value: "FR", label: { fr: "France", en: "France" } },
            { value: "BE", label: { fr: "Belgique", en: "Belgium" } },
            { value: "CI", label: { fr: "Côte d'Ivoire", en: "Ivory Coast" } },
          ],
          validations: [{ validationType: "required", errMsg: "Requis" }],
        },
        {
          // City options change dynamically based on selected country
          formFieldId: "f-city",
          fieldName: "city",
          fieldType: "dropdown",
          label: { fr: "Ville", en: "City" },
          response: { responseValue: "" },
          dynamicFilterRule: {
            dependentFieldName: "country",
            filterType: "exact",
            dataSource: {
              type: "static",
              data: {
                FR: [{ value: "paris", label: "Paris" }, { value: "lyon", label: "Lyon" }],
                BE: [{ value: "bruxelles", label: "Bruxelles" }, { value: "liege", label: "Liège" }],
                CI: [{ value: "abidjan", label: "Abidjan" }, { value: "bouake", label: "Bouaké" }],
              },
            },
          },
        },
        {
          formFieldId: "f-sector",
          fieldName: "sector",
          fieldType: "dropdown",
          label: { fr: "Secteur d'activité", en: "Business sector" },
          response: { responseValue: "" },
          selectOptions: [
            { value: "agri", label: { fr: "Agriculture", en: "Agriculture" } },
            { value: "tech", label: "Tech" },
            { value: "other", label: { fr: "Autre", en: "Other" } },
          ],
          // Reveals a free-text field when "Autre" is selected
          detailInfos: {
            other: {
              label: { fr: "Précisez", en: "Specify" },
              formFields: [
                {
                  formFieldId: "f-sector-detail",
                  fieldName: "sector_detail",
                  fieldType: "text",
                  label: { fr: "Secteur exact", en: "Exact sector" },
                  response: { responseValue: "" },
                },
              ],
            },
          },
        },
      ],
    },

    // ── Step 3 — Passengers (repeat group) ───────────────────────────
    {
      sectionId: "s-passengers",
      title: { fr: "Passagers", en: "Passengers" },
      formFields: [
        {
          formFieldId: "f-passengers",
          fieldName: "passengers",
          fieldType: "text",
          label: { fr: "Passagers", en: "Passengers" },
          response: { responseValue: "" },
          repeatGroup: {
            repeatGroupId: "rg-passengers",
            fieldName: "passengers",
            label: { fr: "Passager", en: "Passenger" },
            minRepeats: 1,
            maxRepeats: 5,
            initialRepeats: 1,
            formFields: [
              {
                formFieldId: "f-p-name",
                fieldName: "passenger_name",
                fieldType: "text",
                label: { fr: "Nom complet", en: "Full name" },
                response: { responseValue: "" },
                width: "half",
                validations: [{ validationType: "required", errMsg: "Requis" }],
              },
              {
                formFieldId: "f-p-age",
                fieldName: "passenger_age",
                fieldType: "number",
                label: { fr: "Âge", en: "Age" },
                response: { responseValue: "" },
                width: "half",
                validations: [{ validationType: "required", errMsg: "Requis" }],
              },
              {
                formFieldId: "f-p-passport",
                fieldName: "passport_scan",
                fieldType: "PDF",
                label: { fr: "Scan passeport", en: "Passport scan" },
                response: { responseValue: "" },
                fileToBucketManage: {
                  uploadOption: {
                    serverDns: "https://bucket.example.com",
                    postApiEndPoint: "/api/files/upload-file",
                    payload: { folder_name: "passports" },
                    bearer: "my-token",
                  },
                  deleteOption: {
                    serverDns: "https://bucket.example.com",
                    deleteApiEndPoint: "/api/files/delete-file",
                    payload: { folder_name: "passports" },
                  },
                },
              },
            ],
          },
          repeatRule: { min: 1, max: 5, prefillEmpty: true },
        },
      ],
    },
  ],
};

export default function RegistrationPage() {
  return (
    <FormEngine
      form={registrationForm}
      formId="registration-2024"
      currentLang="fr"
      submitButtonText="Suivant"
      onSubmit={(data) => {
        // data.first_name, data.last_name, data.email
        // data.country, data.city, data.sector, data.sector_detail?
        // data.passengers: [{ passenger_name, passenger_age, passport_scan }]
        console.log(data);
      }}
    />
  );
}

License

MIT © Lionel TOTON