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

dynamic-form-kit

v2.0.1

Published

Dynamic form renderer — install the library, pass your exported JSON, the form appears.

Downloads

1,975

Readme

dynamic-form-kit

A production-ready dynamic form renderer for React. Pass a JSON config, get a fully functional form with 30+ field types, validation, containers, callbacks, custom components, and theming.

Installation

npm install dynamic-form-kit

Peer dependencies:

npm install react react-dom

Quick Start

import { DynamicForm } from 'dynamic-form-kit';
import 'dynamic-form-kit/style.css';

const fields = [
  { type: 'text', name: 'firstName', label: 'First Name', colSpan: '6', isRequired: true },
  { type: 'text', name: 'lastName', label: 'Last Name', colSpan: '6', isRequired: true },
  { type: 'email', name: 'email', label: 'Email', colSpan: '12', isRequired: true },
];

export default function App() {
  return (
    <DynamicForm
      fields={fields}
      onSubmit={(values) => console.log(values)}
      submitButton={<button type="submit">Submit</button>}
    />
  );
}

DynamicForm Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | fields | FormField[] | required | Field configuration array | | onSubmit | (values) => void | required | Called on valid submit | | submitButton | ReactNode | — | Custom submit button | | defaultValues | object | null | Pre-fill form data | | className | string | — | Container class | | formId | string | — | HTML form id | | formClassName | string | '' | Form element class | | reValidateMode | 'onChange' \| 'onBlur' \| 'onSubmit' | 'onChange' | Re-validation trigger | | onFormValuesChange | (values) => void | — | Called on every change | | themeConfig | { primaryColor: string } | — | Theme colors | | header | ReactNode | — | Content above fields | | footer | ReactNode | — | Content below submit | | children | ReactNode \| Function | — | Content between fields and submit. Function receives { form, formState, values } | | components | Record<string, Component> | — | Custom component registry for type: 'custom' | | slots | Record<string, ReactNode \| Function> | — | Named slot content for type: 'slot' |

Ref Methods

const formRef = useRef(null);
<DynamicForm ref={formRef} fields={fields} onSubmit={handleSubmit} />

formRef.current.submit();                         // Trigger submit
formRef.current.reset();                          // Reset form
formRef.current.getValues();                      // Get all values
formRef.current.validate();                       // Run validation
formRef.current.setValue('fieldName', 'value');    // Set field value
formRef.current.formState;                        // react-hook-form state
formRef.current.getWrapper('fieldName');           // Get wrapper ref

Common Field Properties

Every field type supports these properties:

{
  // ─── Required ───
  type: 'text',                        // Field type (see all types below)
  name: 'fieldName',                   // Unique form key

  // ─── Display ───
  label: 'Field Label',                // Label text
  placeholder: 'Hint...',             // Placeholder
  colSpan: '6',                        // Grid width: '1' to '12' (default: '12')
  helperText: 'Description text',      // Text below field
  hideLabel: false,                    // Hide the label
  className: '',                       // CSS class on wrapper
  disabled: false,                     // Disabled state

  // ─── Validation ───
  isRequired: true,                    // Required field
  defaultValue: '',                    // Initial value

  // ─── Callbacks (available on ALL field types) ───
  onChangeCb: (value) => {},           // Fires when value changes
  onClickCb: () => {},                 // Fires on click
  onBlurCb: (value) => {},             // Fires on blur
  onFocusCb: (value) => {},            // Fires on focus
  onKeyDownCb: (event) => {},          // Fires on keydown (input types)

  // ─── Label Styling ───
  labelColor: '#525E6F',               // Label text color
  labelFontSize: 'xs',                 // xs | sm | base | lg | xl
  labelFontWeight: 'medium',           // normal | medium | semibold | bold
  asteriskPosition: 'end',             // start | end
}

All Field Types

text

{
  type: 'text',
  name: 'firstName',
  label: 'First Name',
  placeholder: 'Enter name',
  colSpan: '6',
  isRequired: true,
  maxLength: 50,
  minLength: 2,
  defaultValue: '',
  validation: 'name',                  // Built-in: name | phone | fax | ssn | zipcode
  autoComplete: 'off',                 // off | on
  inputHeight: '40px',                 // 32px | 40px | 48px | 56px
  bgInputColor: '#ffffff',             // Input background color
  prefix: { type: 'text', content: '$' },        // Text, Phosphor icon, or SVG
  suffix: { type: 'text', content: 'USD' },
  // Custom validation
  validation: '__custom__',
  pattern: '^[A-Z]+$',
  patternMessage: 'Only uppercase letters',
  onChangeCb: (value) => console.log('Changed:', value),
  onBlurCb: (value) => console.log('Blurred:', value),
  onClickCb: () => console.log('Clicked'),
  onFocusCb: (value) => console.log('Focused'),
  onKeyDownCb: (e) => console.log('Key:', e.key),
}

email

{
  type: 'email',
  name: 'email',
  label: 'Email Address',
  placeholder: '[email protected]',
  colSpan: '12',
  isRequired: true,
  onChangeCb: (value) => {},
}

number

{
  type: 'number',
  name: 'age',
  label: 'Age',
  min: 0,
  max: 150,
  step: 1,
  integer: true,                       // Allow only integers
  prefix: { type: 'text', content: '#' },
  suffix: { type: 'text', content: 'years' },
  pattern: '^[0-9]+$',
  patternMessage: 'Numbers only',
  onChangeCb: (value) => {},
}

textarea

{
  type: 'textarea',
  name: 'notes',
  label: 'Notes',
  placeholder: 'Enter notes...',
  rows: 4,                             // Initial rows: 2-10
  maxRows: 10,                         // Max rows before scroll
  maxLength: 500,                      // Shows character counter
  enforceMaxRows: true,                // Prevent resize beyond maxRows
  bgInputColor: '#ffffff',
  onChangeCb: (value) => {},
  onBlurCb: (value) => {},
}

password

{
  type: 'password',
  name: 'password',
  label: 'Password',
  placeholder: 'Enter password',
  isRequired: true,
  onChangeCb: (value) => {},
  onBlurCb: (value) => {},
  onClickCb: () => {},
  onFocusCb: (value) => {},
}

date

{
  type: 'date',
  name: 'birthDate',
  label: 'Date of Birth',
  placeholder: 'Choose Date',
  colSpan: '6',
  isRequired: true,
  dateRestriction: 'past',             // past | future | pastOrToday
  showMonthDropdown: true,
  showYearDropdown: true,
  defaultValue: '__today__',           // Special token for current date
  disabledWhen: 'asSoonAsPossible',    // Disabled when this field is true
  minDateField: 'startDate',           // Min date from another field
  minDateOffset: 1,                    // Days after minDateField
  onChangeCb: (date) => {},
  onClickCb: () => {},
  onBlurCb: (date) => {},
}

time

{
  type: 'time',
  name: 'appointmentTime',
  label: 'Time',
  colSpan: '6',
  defaultValue: '14:30',              // HH:mm format (auto-fills current time if not set)
  onChangeCb: (value) => {},
  onClickCb: () => {},                 // Also opens native time picker
}

select

{
  type: 'select',
  name: 'country',
  label: 'Country',
  placeholder: 'Select country',
  colSpan: '6',
  isRequired: true,
  searchable: true,                    // Enable search in options
  bgInputColor: '#ffffff',
  contentClassName: '',                // Dropdown CSS class
  options: [
    { value: 'us', label: 'United States' },
    { value: 'uk', label: 'United Kingdom' },
  ],
  // Conditional options
  dependsOn: 'region',
  conditionalOptions: {
    americas: [{ value: 'us', label: 'US' }],
    europe: [{ value: 'uk', label: 'UK' }],
  },
  clearFieldsOnChange: ['city', 'state'],
  // Custom action option
  customActionOption: true,
  customActionLabel: '+ Add New',
  customActionStartIcon: { type: 'phosphor', name: 'Plus' },
  customActionEndIcon: { type: 'text', content: '>' },
  onChangeCb: (value) => {},
  onClickCb: (value) => {},
  onBlurCb: (value) => {},
}

multiSelect

{
  type: 'multiSelect',
  name: 'skills',
  label: 'Skills',
  placeholder: 'Select skills',
  colSpan: '12',
  isRequired: true,
  searchPlaceholder: 'Search...',
  noOptionsLabel: 'No skills found',
  minSelected: 1,
  maxSelected: 5,
  triggerClassName: '',
  contentClassName: '',
  options: [
    { value: 'react', label: 'React' },
    { value: 'node', label: 'Node.js' },
  ],
  dependsOn: 'department',
  conditionalOptions: { /* same as select */ },
  onChangeCb: (values) => {},
  onBlurCb: () => {},
}

asyncSelect

{
  type: 'asyncSelect',
  name: 'doctor',
  label: 'Select Doctor',
  colSpan: '6',
  url: '/api/doctors',
  labelKey: 'fullName',
  valueKey: 'id',
  dataKey: 'data',
  searchParam: 'search',
  pageParam: 'page',
  pageSizeParam: 'pageSize',
  pageSize: 20,
  debounceDelay: 300,
  totalPagesKey: 'totalPages',
  countKey: 'totalCount',
  labelKey1: 'firstName',              // Dual label support
  labelKey2: 'lastName',
  valueKey1: 'id',
  dependsOn: 'department',
  initialLabel: 'Dr. Smith',
  resolveIdUrl: '/api/doctors/:id',
  resolveIdToObject: false,
  onChangeCb: (value) => {},
}

checkbox

{
  type: 'checkbox',
  name: 'agreeTerms',
  label: 'I agree to terms and conditions',
  colSpan: '12',
  isRequired: true,
  checkboxSize: 'default',             // sm | default | lg
  labelPosition: 'right',             // right | left
  onChangeCb: (checked) => {},
  onClickCb: () => {},
  onBlurCb: (checked) => {},
}

checkboxGroup

{
  type: 'checkboxGroup',
  name: 'hobbies',
  label: 'Hobbies',
  colSpan: '12',
  isRequired: true,
  options: [
    { value: 'reading', label: 'Reading' },
    { value: 'sports', label: 'Sports' },
    { value: 'music', label: 'Music' },
  ],
  onChangeCb: (values) => {},          // Array of selected values
  onClickCb: () => {},
  onBlurCb: (values) => {},
}

tagCheckboxGroup

{
  type: 'tagCheckboxGroup',
  name: 'tags',
  label: 'Select Tags',
  colSpan: '12',
  options: [
    { value: 'urgent', label: 'Urgent' },
    { value: 'review', label: 'Review' },
  ],
  onChangeCb: (values) => {},
  onClickCb: () => {},
  onBlurCb: (values) => {},
}

radio

{
  type: 'radio',
  name: 'gender',
  label: 'Gender',
  colSpan: '6',
  radioOrientation: 'horizontal',     // horizontal | vertical
  options: [
    { value: 'male', label: 'Male' },
    { value: 'female', label: 'Female' },
    { value: 'other', label: 'Other' },
  ],
  onChangeCb: (value) => {},
}

radioButtonGroup

{
  type: 'radioButtonGroup',
  name: 'priority',
  label: 'Priority',
  colSpan: '12',
  wrapperClassName: 'gap-4',
  labelClassName: '',
  options: [
    { value: 'low', label: 'Low' },
    { value: 'medium', label: 'Medium' },
    { value: 'high', label: 'High' },
  ],
  onChangeCb: (value) => {},
  onClickCb: (value) => {},
  onBlurCb: (value) => {},
}

switch / toggle

{
  type: 'switch',                      // or 'toggle'
  name: 'notifications',
  label: 'Enable Notifications',
  colSpan: '6',
  activeLabel: 'On',
  inactiveLabel: 'Off',
  showSwitchLabel: true,
  onChangeCb: (checked) => {},
  onClickCb: () => {},
  onBlurCb: (checked) => {},
}

upload

{
  type: 'upload',
  name: 'avatar',
  label: 'Profile Photo',
  colSpan: '6',
  isRequired: true,
  uploadVariant: 'profile',            // profile (circular) | document (drag & drop)
  accept: 'image/jpeg,image/png',
  maxSize: 5242880,                    // Bytes (5MB)
  maxDimensions: '800x800px',
  formatText: 'JPG, PNG (max 5MB)',    // Override auto-generated text
  hidePreview: false,
  onChangeCb: (file) => {},
  onClickCb: () => {},
  onBlurCb: () => {},
}

documentUpload

{
  type: 'documentUpload',
  name: 'resume',
  label: 'Upload Resume',
  colSpan: '12',
  accept: 'application/pdf',
  maxSize: 10485760,
  formatText: 'PDF only, max 10MB',
  onChangeCb: (file) => {},
}

multiDocumentUpload

{
  type: 'multiDocumentUpload',
  name: 'documents',
  label: 'Supporting Documents',
  colSpan: '12',
  accept: 'image/jpeg,image/png,application/pdf',
  maxSize: 5242880,
  maxFiles: 5,
  onChangeCb: (files) => {},
}

signaturePad

{
  type: 'signaturePad',
  name: 'signature',
  label: 'Signature',
  colSpan: '12',
  isRequired: true,
  penColor: '#1a1a1a',                 // Stroke color
  backgroundColor: '#F8FAFF',          // Canvas background
  canvasHeight: 200,                   // 150 | 200 | 250 | 300
  maxWidth: 2,                         // Line width: 1 | 2 | 3 | 4
  onChangeCb: (dataUrl) => {},         // Base64 data URL
}

separator

{
  type: 'separator',
  colSpan: '12',
  orientation: 'horizontal',          // horizontal | vertical
  content: 'OR',                       // Optional text in separator
}

label

{
  type: 'label',
  label: 'Section Label',
  colSpan: '12',
  isHelperText: false,                 // Render as helper text style
}

title

{
  type: 'title',
  label: 'Section Title',
  colSpan: '12',
  icon: 'buildingsIcon',              // Icon name or { type: 'phosphor', name: '...' }
  badge: 'New',
  badgeClassName: 'bg-blue-100 text-blue-800 text-xs px-2 py-0.5 rounded-full',
}

text-content

{
  type: 'text-content',
  content: 'This is informational text that appears inline.',
  colSpan: '12',
}

button

{
  type: 'button',
  label: 'Calculate Total',
  colSpan: '6',
  variant: 'outline',                  // default | outline | ghost | secondary | destructive | link
  icon: { type: 'phosphor', name: 'Calculator' },
  disabled: false,
  onClickCb: (form) => {
    const qty = form.getValues('quantity');
    form.setValue('total', qty * 100);
  },
}

hidden

{
  type: 'hidden',
  name: 'userId',
  defaultValue: '12345',
}

image

{
  type: 'image',
  colSpan: '12',
  iconName: 'buildingsIcon',          // Icon from Icons component
  src: 'https://example.com/img.png', // Image URL (used when iconName not set)
  alt: 'Image description',
  width: 200,
  height: 100,
  color: '#000000',                    // Icon color
  imageClassName: 'max-w-full h-auto rounded-lg',
}

embedPdf

{
  type: 'embedPdf',
  colSpan: '12',
  url: 'https://example.com/document.pdf',  // Required — empty URL shows placeholder
  label: 'Document',
  height: '500px',
  iframeClassName: '',
}

Containers

card

{
  type: 'card',
  name: 'personalInfo',
  label: 'Personal Information',
  colSpan: '12',

  // Visual styling
  containerStyle: {
    border: 'thin',                    // default | none | thin | medium
    borderColor: '#DEE4ED',
    borderRadius: 'xl',               // none | sm | md | lg | xl | 2xl | 3xl
    shadow: 'sm',                      // none | sm | md | lg | xl
    padding: '4',                      // 0 | 2 | 3 | 4 | 5 | 6 | 8
    gap: '4',                          // 0 | 2 | 3 | 4 | 5 | 6
    bgColor: '#ffffff',
  },

  // Header
  cardHeader: {
    show: true,
    variant: 'primary',                // default | filled | primary | subtle | transparent
    bgColor: '#00559C',
    textColor: '#ffffff',
    titleSize: 'base',                 // xs | sm | base | lg | xl
    titleWeight: 'semibold',           // normal | medium | semibold | bold
    align: 'between',                  // start | center | end | between
    padding: '3',                      // 2 | 3 | 4 | 5
    icon: { type: 'phosphor', name: 'User' },
    buttons: [
      { label: 'Edit', variant: 'outline', onClickCb: (form) => {} },
    ],
  },

  // Footer
  cardFooter: {
    show: true,
    align: 'end',                      // start | center | end | between
    buttons: [
      { label: 'Cancel', variant: 'outline', onClickCb: (form) => {} },
      { label: 'Save', variant: 'default', onClickCb: (form) => {} },
    ],
  },

  // Children
  children: [
    { type: 'text', name: 'firstName', label: 'First Name', colSpan: '6', isRequired: true },
    { type: 'text', name: 'lastName', label: 'Last Name', colSpan: '6' },
    { type: 'email', name: 'email', label: 'Email', colSpan: '12' },
  ],
}

wrapper

Supports the same containerStyle, cardHeader, cardFooter as Card.

{
  type: 'wrapper',
  name: 'address',
  label: 'Address',
  colSpan: '12',
  containerStyle: { border: 'thin', borderRadius: 'lg', padding: '4' },
  cardHeader: { show: true, variant: 'subtle' },
  children: [
    { type: 'text', name: 'street', label: 'Street', colSpan: '12' },
    { type: 'text', name: 'city', label: 'City', colSpan: '6' },
    { type: 'text', name: 'state', label: 'State', colSpan: '6' },
  ],
}

Repeatable Wrapper (Array)

{
  type: 'wrapper',
  name: 'contacts',
  label: 'Emergency Contacts',
  colSpan: '12',
  isArray: true,
  showIndex: true,
  arrayItemConfig: {
    buttons: {
      add: { label: '+ Add Contact' },
      remove: { label: 'Remove' },
    },
    enableAddOnPartialFill: true,
  },
  filterArrayItems: true,
  filterArrayItemsKey: 'name',
  children: [{
    children: [
      { type: 'text', name: 'name', label: 'Name', colSpan: '6', isRequired: true },
      { type: 'text', name: 'phone', label: 'Phone', colSpan: '6', validation: 'phone' },
    ],
  }],
}

accordion

{
  type: 'accordion',
  name: 'advancedSettings',
  label: 'Advanced Settings',
  colSpan: '12',
  defaultOpen: true,
  icon: { type: 'phosphor', name: 'Gear' },
  children: [
    { type: 'text', name: 'apiKey', label: 'API Key', colSpan: '12' },
    { type: 'switch', name: 'debugMode', label: 'Debug Mode' },
  ],
}

Accordion Groups (with points)

// Accordion Checkbox Group
{
  type: 'accordionCheckboxGroup',
  name: 'symptoms',
  label: 'Symptoms',
  options: [
    { value: 'fever', label: 'Fever', points: 2 },
    { value: 'cough', label: 'Cough', points: 1 },
  ],
  onChangeCb: (values) => {},
  onClickCb: (value) => {},
  onBlurCb: () => {},
}

// Accordion Radio Group
{
  type: 'accordionRadioGroup',
  name: 'severity',
  label: 'Severity',
  options: [
    { value: 'mild', label: 'Mild', points: 1 },
    { value: 'severe', label: 'Severe', points: 3 },
  ],
  onChangeCb: (valueObj) => {},        // { value, points }
  onClickCb: (value) => {},
  onBlurCb: () => {},
}

// Accordion Select
{
  type: 'accordionSelect',
  name: 'treatment',
  label: 'Treatment',
  options: [
    { value: 'medication', label: 'Medication', points: 1 },
    { value: 'surgery', label: 'Surgery', points: 5 },
  ],
  onChangeCb: (value) => {},
  onClickCb: (value) => {},
  onBlurCb: (value) => {},
}

Table

{
  type: 'table',
  name: 'medications',
  label: 'Medications',
  colSpan: '12',
  editable: true,
  addMoreRows: true,
  addButtonText: '+ Add Medication',
  removeButtonText: 'Remove',
  emptyMessage: 'No medications added',
  minRows: 1,
  maxRows: 10,

  // Display
  size: 'md',                          // sm | md | lg
  striped: true,                       // Alternating row colors
  sortable: true,                      // Enable column sorting (also per-column)

  // Pagination
  pagination: true,
  pageSize: 15,
  pageSizeOptions: [10, 15, 20, 50],
  showPageInfo: true,                  // "1-15 of 34 Rows"
  showPageSizeSelector: true,          // Rows per page dropdown
  onPageChangeCb: (page, limit) => {}, // Server-side pagination callback

  // Row click
  onRowClick: (row, index) => {},

  // Columns
  columns: [
    {
      key: 'name',
      header: 'Medication',
      type: 'label',                   // label | text | number | date | textarea | select | radio-group | signaturePad | icon | badge
      editable: false,
      sortable: true,                  // Per-column sorting
      width: '200px',
      headerAlign: 'left',            // left | center | right
      cellAlign: 'left',
      // Label column styling
      fontSize: 'sm',                  // xs | sm | base | lg
      fontWeight: 'semibold',
      color: '#27313F',
    },
    {
      key: 'status',
      header: 'Status',
      type: 'badge',                   // Colored status pill
      badgeStyle: 'pill',              // pill | dot | outline
      colorMap: {
        Active: '#10B981',
        Pending: '#F59E0B',
        Inactive: '#9AA8BC',
      },
    },
    {
      key: 'icon',
      header: '',
      type: 'icon',
      iconSize: 20,
      iconColor: '#728197',
      defaultIcon: { type: 'phosphor', name: 'Check' },
      iconMap: {                       // Map cell values to icons
        active: 'checkCircleIcon',
        inactive: 'xCircleIcon',
      },
    },
    {
      key: 'dosage',
      header: 'Dosage',
      type: 'text',
      editable: true,
      sortable: true,
    },
    {
      key: 'frequency',
      header: 'Frequency',
      type: 'select',
      editable: true,
      options: [
        { value: 'daily', label: 'Daily' },
        { value: 'weekly', label: 'Weekly' },
      ],
    },
  ],

  // Action column
  showActionColumn: true,
  actionsHeader: 'Action',
  actionsWidth: '100px',
  actionStyle: 'popover',             // popover | buttons

  // Action items
  rowActions: [
    {
      type: 'button',                 // button | icon | separator
      label: 'Edit',
      variant: 'ghost',               // ghost | outline | default | danger | link
      startIcon: { type: 'phosphor', name: 'PencilSimple' },
      endIcon: { type: 'phosphor', name: 'ArrowRight' },
      action: 'custom',               // custom | remove
      onClickCb: (row, rowIndex, form) => {
        console.log('Edit:', row);
      },
    },
    { type: 'separator' },
    {
      type: 'icon',
      label: 'Delete',
      startIcon: 'trashIcon',
      variant: 'danger',
      action: 'remove',                // Built-in: removes the row
    },
  ],

  // Default data
  data: [
    { id: 1, name: 'Aspirin', dosage: '500mg', frequency: 'daily', status: 'Active' },
  ],
}

Custom Components

Method 1: Inline render function

{
  type: 'custom',
  name: 'preview',
  colSpan: '12',
  render: ({ form }) => (
    <div className="p-4 bg-gray-50 rounded">
      <p>Total: ${form.watch('amount') || 0}</p>
    </div>
  ),
}

Method 2: Component reference

function RichEditor({ field, form }) {
  return <MyEditor value={form.watch(field.name)} onChange={(v) => form.setValue(field.name, v)} />;
}

{ type: 'custom', name: 'bio', colSpan: '12', component: RichEditor }

Method 3: Component registry (JSON-safe)

// Register once
<DynamicForm
  fields={formJson}
  onSubmit={handleSubmit}
  components={{
    AddressAutocomplete: MyAddressComponent,
    PaymentWidget: MyPaymentComponent,
  }}
/>

// Field JSON (export from Form Builder)
{ "type": "custom", "name": "address", "componentKey": "AddressAutocomplete", "componentProps": { "apiUrl": "/api/places" }, "colSpan": "12" }

Each custom component receives: { field, form, ...componentProps }

HTML Block

{ type: 'html', colSpan: '12', htmlContent: '<div class="p-4 bg-yellow-50 rounded"><p>Important notice</p></div>', htmlClassName: '' }

Grid Layout

12-column grid. Set colSpan on each field:

'12' --> full width
'6'  --> half (2 per row)
'4'  --> third (3 per row)
'3'  --> quarter (4 per row)

Fields wrap automatically when total exceeds 12.

Theming

<DynamicForm
  fields={fields}
  onSubmit={handleSubmit}
  themeConfig={{ primaryColor: '#7C3AED' }}
/>

Sets CSS variables --fb-primary, --fb-secondary, --fb-secondary-light, --fb-secondary-lighter used by all components.

Utilities

import { getZodValidationSchema, buildDefaultValues, buildMergedDefaultValues } from 'dynamic-form-kit';

const schema = getZodValidationSchema(fields);         // Zod schema for server validation
const defaults = buildDefaultValues(fields);             // Default values from config
const merged = buildMergedDefaultValues(fields, data);   // Merge with existing data

Complete Example

import { DynamicForm } from 'dynamic-form-kit';
import 'dynamic-form-kit/style.css';

const patientForm = [
  {
    type: 'card', name: 'personal', label: 'Patient Info', colSpan: '12',
    containerStyle: { border: 'thin', borderRadius: 'xl', shadow: 'sm', padding: '4' },
    cardHeader: { show: true, variant: 'primary', titleSize: 'base' },
    cardFooter: { show: true, align: 'end', buttons: [
      { label: 'Cancel', variant: 'outline' },
      { label: 'Save', variant: 'default' },
    ]},
    children: [
      { type: 'text', name: 'firstName', label: 'First Name', colSpan: '6', isRequired: true, onChangeCb: (v) => console.log(v) },
      { type: 'text', name: 'lastName', label: 'Last Name', colSpan: '6', isRequired: true },
      { type: 'date', name: 'dob', label: 'DOB', colSpan: '6', dateRestriction: 'past', defaultValue: '__today__' },
      { type: 'select', name: 'gender', label: 'Gender', colSpan: '6', options: [
        { value: 'male', label: 'Male' }, { value: 'female', label: 'Female' },
      ]},
    ],
  },
  { type: 'signaturePad', name: 'signature', label: 'Signature', colSpan: '12', isRequired: true, penColor: '#1a1a1a', canvasHeight: 200 },
  { type: 'checkbox', name: 'consent', label: 'I consent to treatment', colSpan: '12', isRequired: true, onChangeCb: (v) => console.log('Consent:', v) },
];

export default function App() {
  return (
    <DynamicForm
      fields={patientForm}
      onSubmit={(values) => fetch('/api/patients', { method: 'POST', body: JSON.stringify(values) })}
      themeConfig={{ primaryColor: '#00559C' }}
      submitButton={<button type="submit" className="w-full py-3 bg-blue-600 text-white rounded-lg">Register</button>}
      header={<h1 className="text-2xl font-bold">Patient Registration</h1>}
    >
      {({ values }) => values.consent && <p className="text-green-600 text-sm">Consent given</p>}
    </DynamicForm>
  );
}

License

MIT