dynamic-form-kit
v2.0.1
Published
Dynamic form renderer — install the library, pass your exported JSON, the form appears.
Downloads
1,975
Maintainers
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-kitPeer dependencies:
npm install react react-domQuick 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 refCommon 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),
}{
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 dataComplete 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
