Configuration-driven forms with a built-in rules engine
Formosaic is a React form library where you define forms as JSON and a built-in rules engine handles the rest. One config object controls field rendering, conditional logic, validation, multi-step wizards, and auto-save. Swap between 12 UI frameworks — MUI, Ant Design, Fluent UI, Chakra, Mantine, Radix, and more — without rewriting form logic.
Define your forms as a single IFormConfig JSON object — field definitions, declarative rules with 20 condition operators, validation, ordering — and the library handles rendering, validation, auto-save, and field interactions automatically.
Use this if you need:
- Forms defined as JSON/config objects, not JSX -- field types, labels, validations, and rules declared as data
- A rules engine where field A changing to value X makes field B required, field C hidden, and field D's dropdown options change -- all declared, not coded
- Multi-step wizards with conditional step visibility and cross-step rules
- Auto-save with debounce, retry, and abort -- not just "submit on click"
- To swap UI libraries (Fluent UI, MUI, headless HTML, custom) without rewriting form logic
Don't use this if you need:
- Simple forms with 3-5 static fields -- use react-hook-form directly
- Pure JSON Schema rendering with no rules engine -- use RJSF (but if you want RJSF's schema format with our rules engine, use
fromRjsfSchema()to migrate) - Headless form state with zero opinions -- use TanStack Form
See the comparison guide for a detailed comparison with migration paths.
| Feature | Formosaic | FormEngine.io | RJSF | TanStack Form | uniforms |
|---|---|---|---|---|---|
| Config-driven (JSON/data) | Yes | Yes | Yes | No | Yes |
| Declarative rules engine | Yes (20 ops) | Partial | Partial | No | No |
| UI adapter system | 11 adapters | 4 adapters | 5 themes | Headless | 6 bridges |
| Computed values | $values, $fn |
MobX-based | No | No | No |
| Wizard / multi-step | Built-in | Layout-based | Add-on | Manual | No |
| Visual form builder | No | Yes (paid) | No | No | No |
| AI form generation | No | ChatGPT GPT | No | No | No |
| Schema import (JSON/Zod) | Both | Proprietary | JSON Schema | No | JSON Schema |
| License | MIT | MIT core / Paid builder | Apache 2.0 | MIT | MIT |
| Bundle (core) | ~114 KB | ~189 KB | ~85 KB | ~12 KB | ~45 KB |
| Package | Description | Size |
|---|---|---|
@formosaic/core |
UI-agnostic rules engine, form orchestration, validation, analytics, devtools. React + react-hook-form only, no UI library dependency. | ~114 KB ESM |
@formosaic/fluent |
Fluent UI v9 field components (27 field types). | ~39 KB ESM |
@formosaic/mui |
Material UI field components (27 field types). | ~39 KB ESM |
@formosaic/headless |
Unstyled semantic HTML field components (27 field types). | ~36 KB ESM |
@formosaic/antd |
Ant Design v5 field components (27 field types). | ~24 KB ESM |
@formosaic/chakra |
Chakra UI v3 field components (27 field types). | ~24 KB ESM |
@formosaic/mantine |
Mantine v7 field components (27 field types). | ~24 KB ESM |
@formosaic/atlaskit |
Atlassian Design System field components (27 field types). | ~22 KB ESM |
@formosaic/base-web |
Uber Base Web field components (27 field types). | ~22 KB ESM |
@formosaic/heroui |
HeroUI field components (27 field types). | ~22 KB ESM |
@formosaic/radix |
Radix UI primitives field components (27 field types). Unstyled. | ~32 KB ESM |
@formosaic/react-aria |
React Aria Components field components (27 field types). | ~31 KB ESM |
@formosaic/examples |
6 example apps: login+MFA, checkout wizard, data entry, patient-intake, job-application, expense-report. | -- |
# With Fluent UI
npm install @formosaic/core @formosaic/fluent
# Or with MUI
npm install @formosaic/core @formosaic/mui @mui/material @emotion/react @emotion/styled
# Or headless (no UI framework)
npm install @formosaic/core @formosaic/headless
# Or with Ant Design
npm install @formosaic/core @formosaic/antd antd dayjs
# Or with Mantine
npm install @formosaic/core @formosaic/mantine @mantine/core @mantine/hooks
# Or with Radix UI (unstyled primitives, great for Tailwind/shadcn)
npm install @formosaic/core @formosaic/radix @radix-ui/react-checkbox @radix-ui/react-radio-group @radix-ui/react-select @radix-ui/react-slider @radix-ui/react-switch
# Or with React Aria (accessibility-first)
npm install @formosaic/core @formosaic/react-aria react-aria-componentsimport {
RulesEngineProvider,
InjectedFieldProvider,
Formosaic,
} from "@formosaic/core";
import { createFluentFieldRegistry } from "@formosaic/fluent";
// Or: import { createMuiFieldRegistry } from "@formosaic/mui";
// Or: import { createHeadlessFieldRegistry } from "@formosaic/headless";
const formConfig = {
version: 2 as const,
fields: {
name: { type: "Textbox", label: "Name", required: true },
status: {
type: "Dropdown",
label: "Status",
options: [
{ value: "Active", label: "Active" },
{ value: "Inactive", label: "Inactive" },
],
},
notes: { type: "Textarea", label: "Notes" },
},
fieldOrder: ["name", "status", "notes"],
};
function App() {
return (
<RulesEngineProvider>
<InjectedFieldProvider injectedFields={createFluentFieldRegistry()}>
<Formosaic
configName="myForm"
formConfig={formConfig}
defaultValues={{ name: "", status: "Active", notes: "" }}
saveData={async (data) => {
console.log("Saving:", data);
return data;
}}
/>
</InjectedFieldProvider>
</RulesEngineProvider>
);
}- Open in StackBlitz -- edit and run the checkout wizard example in your browser
- Storybook -- interactive component gallery covering all 27 field types for the Fluent UI adapter. Multi-adapter stories are planned.
Every form is defined by an IFormConfig object containing a dictionary of IFieldConfig entries. Each config specifies:
type-- Which field type to render ("Textbox","Dropdown","Toggle", etc.)label-- Display labelrequired/hidden/readOnly-- Default field statesrules-- Declarative rules with rich conditions (when/then/else) that change field states based on other field valuesoptions-- Dropdown/select options as{ value, label }pairsvalidate-- Validation rules referencing the unified validator registrycomputedValue-- Expressions like"$values.qty * $values.price"or"$fn.calculateTotal()"items-- Field array item definitions (fullIFieldConfigper item field)config-- Arbitrary metadata passed through to the field component
Rules are declarative -- defined as IRule[] on each field config, not imperative code. Each rule has a when condition, a then effect, and an optional else effect.
When a field value changes, the engine:
- Identifies transitively affected fields via the dependency graph
- Re-evaluates rules for affected fields only (incremental evaluation)
- Resolves conflicts via priority (higher priority rule wins)
- Applies effects (required, hidden, readOnly, component swap, options, validation, computed value, setValue)
- Dispatches to the rules engine reducer for React re-render
The engine includes circular dependency detection via Kahn's algorithm and config validation for dev-mode diagnostics.
20 condition operators: equals, notEquals, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual, contains, notContains, startsWith, endsWith, in, notIn, isEmpty, isNotEmpty, matches, arrayContains, arrayNotContains, arrayLengthEquals, arrayLengthGreaterThan, arrayLengthLessThan
Logical operators: and, or, not (composable condition trees)
const formConfig = {
version: 2 as const,
fields: {
type: {
type: "Dropdown",
label: "Type",
options: [
{ value: "bug", label: "Bug" },
{ value: "feature", label: "Feature" },
],
rules: [
{
when: { field: "type", operator: "equals", value: "bug" },
then: { fields: { severity: { required: true, hidden: false } } },
else: { fields: { severity: { hidden: true } } },
priority: 1,
},
],
},
severity: {
type: "Dropdown",
label: "Severity",
hidden: true,
options: [
{ value: "low", label: "Low" },
{ value: "high", label: "High" },
],
},
},
fieldOrder: ["type", "severity"],
};Combine conditions with and, or, and not:
rules: [
{
when: {
operator: "and",
conditions: [
{ field: "type", operator: "equals", value: "bug" },
{ field: "priority", operator: "greaterThanOrEqual", value: 3 },
],
},
then: { assignee: { required: true } },
},
]Use computedValue with $values, $fn, and $parent expressions:
fields: {
qty: { type: "Number", label: "Quantity" },
price: { type: "Number", label: "Unit Price" },
total: {
type: "ReadOnly",
label: "Total",
computedValue: "$values.qty * $values.price",
},
createdDate: {
type: "ReadOnly",
label: "Created",
computedValue: "$fn.setDate()",
},
}Split forms into wizard steps with conditional visibility and per-step validation:
import { WizardForm } from "@formosaic/core";
const formConfig = {
version: 2 as const,
fields: { /* ... */ },
wizard: {
steps: [
{ id: "basics", title: "Basic Info", fields: ["name", "type"] },
{
id: "details",
title: "Details",
fields: ["severity", "description"],
visibleWhen: { field: "type", operator: "equals", value: "bug" },
},
{ id: "review", title: "Review", fields: ["notes"] },
],
validateOnStepChange: true,
},
};
<WizardForm
wizardConfig={formConfig.wizard}
entityData={formValues}
renderStepContent={(fields) => <FieldRenderer fields={fields} />}
renderStepNavigation={({ goNext, goPrev, canGoNext, canGoPrev }) => (
<nav>
<button onClick={goPrev} disabled={!canGoPrev}>Back</button>
<button onClick={goNext} disabled={!canGoNext}>Next</button>
</nav>
)}
/>All fields stay in a single react-hook-form context. Steps control which fields are visible. Cross-step rules work automatically.
Add "add another" patterns for addresses, line items, etc.:
import { FieldArray } from "@formosaic/core";
<FieldArray
fieldName="contacts"
config={{
type: "FieldArray",
label: "Contacts",
items: {
name: { type: "Textbox", label: "Name", required: true },
email: { type: "Textbox", label: "Email", validate: [{ name: "email" }] },
},
minItems: 1,
maxItems: 5,
}}
renderItem={(fieldNames, index, remove) => (
<div key={index}>
{/* fieldNames = ["contacts.0.name", "contacts.0.email"] */}
<FieldRenderer fields={fieldNames} />
<button onClick={remove}>Remove</button>
</div>
)}
renderAddButton={(append, canAdd) => (
<button onClick={append} disabled={!canAdd}>Add Contact</button>
)}
/>The library uses a component injection system for field rendering. Core provides the orchestration, and UI packages provide the field implementations:
// Use built-in Fluent UI fields
import { createFluentFieldRegistry } from "@formosaic/fluent";
// Or use MUI fields (swap with one line)
import { createMuiFieldRegistry } from "@formosaic/mui";
// Or use headless semantic HTML fields
import { createHeadlessFieldRegistry } from "@formosaic/headless";
// Pass via the injectedFields prop
<InjectedFieldProvider injectedFields={createFluentFieldRegistry()}>
// Or mix in custom fields
<InjectedFieldProvider injectedFields={{
...createFluentFieldRegistry(),
MyCustomField: <MyCustomField />,
}}>14 built-in validators plus support for custom sync, async, and cross-field validators via the unified registerValidators() API.
Registering a custom validator — registerValidators() takes a map of name to ValidatorFn. Use this for any validator that isn't already built in, including async server-side checks:
import { registerValidators } from "@formosaic/core";
registerValidators({
// Sync custom validator
AlphaOnly: (value) => {
if (typeof value !== "string") return undefined;
return /^[a-zA-Z]+$/.test(value) ? undefined : "Letters only";
},
// Async validator — receives an AbortSignal in the context arg
CheckUniqueEmail: async (value, _params, { signal }) => {
const response = await fetch(`/api/check-email?email=${value}`, { signal });
const { exists } = await response.json();
return exists ? "Email already in use" : undefined;
},
});Using factory helpers — the create*Rule factories return an IValidationRule that you place directly into a field's validate: array. They are a shorthand for the equivalent { name, params, message } object:
import {
createMinLengthRule,
createMaxLengthRule,
createNumericRangeRule,
createPatternRule,
createRequiredIfRule,
} from "@formosaic/core";
fields: {
username: {
type: "Textbox",
label: "Username",
validate: [
createMinLengthRule(3),
createPatternRule("^[a-zA-Z]+$", "Letters only"),
],
},
}Referencing validators (built-in or registered) by name — set the name on the rule and (optionally) params, message, async, debounceMs, or when:
fields: {
email: {
type: "Textbox",
label: "Email",
validate: [
{ name: "email" },
{ name: "CheckUniqueEmail", async: true, debounceMs: 500 },
],
},
username: {
type: "Textbox",
label: "Username",
validate: [
{ name: "minLength", params: { min: 3 } },
{ name: "AlphaOnly" },
],
},
}Built-in validators: EmailValidation, PhoneNumberValidation, YearValidation, Max150KbValidation, Max32KbValidation, isValidUrl, NoSpecialCharactersValidation, CurrencyValidation, UniqueInArrayValidation. Factory helpers: createMinLengthRule, createMaxLengthRule, createNumericRangeRule, createPatternRule, createRequiredIfRule.
Use registerValidatorMetadata() to attach human-readable metadata (label, description, parameter schema) to validators for display in tooling or documentation:
import { registerValidatorMetadata } from "@formosaic/core";
registerValidatorMetadata("CheckUniqueEmail", {
label: "Unique Email",
description: "Checks that the email address is not already in use",
});All user-facing strings are localizable:
import { registerLocale } from "@formosaic/core";
registerLocale({
required: "Obligatoire",
save: "Sauvegarder",
cancel: "Annuler",
saving: "Sauvegarde en cours...",
invalidEmail: "Adresse e-mail invalide",
// Partial registration -- unspecified keys fall back to English
});Track form lifecycle events via IAnalyticsCallbacks in form settings:
const formConfig: IFormConfig = {
version: 2,
fields: { /* ... */ },
settings: {
analytics: {
onFieldFocus: (fieldName) => console.log("Focus:", fieldName),
onFieldBlur: (fieldName, timeSpentMs) => console.log("Blur:", fieldName, timeSpentMs),
onFieldChange: (fieldName, oldValue, newValue) => console.log("Change:", fieldName),
onValidationError: (fieldName, errors) => console.log("Validation:", fieldName, errors),
onFormSubmit: (values, durationMs) => console.log("Submit:", durationMs, "ms"),
onFormAbandonment: (filledFields, emptyRequired) => console.log("Abandoned:", emptyRequired),
onWizardStepChange: (from, to) => console.log("Step:", from, "->", to),
onRuleTriggered: (event) => console.log("Rule:", event),
},
},
};The useFormAnalytics hook wraps these callbacks into stable, memoized functions with automatic timing (field focus duration, form completion time).
A collapsible dev-only panel with 7 tabs for debugging form state at runtime:
| Tab | Description |
|---|---|
| Rules | Current runtime state of every field (type, required, hidden, readOnly, active rules) |
| Values | Live JSON dump of all form values |
| Errors | Current validation errors |
| Graph | Text representation of the dependency graph |
| Perf | Per-field render counts, hot field detection, total form renders (via RenderTracker) |
| Deps | Sortable dependency table with effect types, cycle detection |
| Timeline | Chronological event log with filtering (via EventTimeline) |
import { FormDevTools } from "@formosaic/core";
<FormDevTools
configName="myForm"
formState={runtimeFormState}
formValues={formValues}
formErrors={formErrors}
dirtyFields={dirtyFields}
enabled={process.env.NODE_ENV === "development"}
/>Catch configuration errors early:
import { validateFieldConfigs } from "@formosaic/core";
const errors = validateFieldConfigs(fieldConfigs, registeredComponentTypes);
// Returns: missing dependency targets, unregistered components,
// unregistered validators, circular dependencies, missing dropdown optionsEach field is individually wrapped in a FormErrorBoundary so a single field crash does not take down the entire form:
import { FormErrorBoundary } from "@formosaic/core";
<FormErrorBoundary
fallback={(error, resetErrorBoundary) => (
<div>
<p>Field failed to render: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
onError={(error, errorInfo) => console.error("Field error:", error)}
>
<MyField />
</FormErrorBoundary>This is built into the core rendering pipeline -- you do not need to add it yourself unless you want custom error handling.
By default, forms auto-save on every field change (debounced). Set isManualSave={true} for explicit save control:
// Auto-save (default) -- saves on every field change with debounce
<Formosaic
configName="myForm"
formConfig={formConfig}
defaultValues={defaultValues}
saveData={async (data) => { await api.save(data); return data; }}
/>
// Manual save -- shows Save/Cancel buttons, no auto-save
<Formosaic
configName="myForm"
formConfig={formConfig}
defaultValues={defaultValues}
isManualSave={true}
saveData={async (data) => { await api.save(data); return data; }}
/>
// Manual save with custom button
<Formosaic
isManualSave={true}
renderSaveButton={({ onSave, isDirty, isSubmitting }) => (
<button onClick={onSave} disabled={!isDirty || isSubmitting}>
Save Changes
</button>
)}
// ... other props
/>Formosaic includes robust save handling:
- AbortController cancels previous in-flight saves when a new save is triggered
- Configurable timeout via
saveTimeoutMsprop (default 30 seconds) - Retry with exponential backoff via
maxSaveRetriesprop (default 3 retries)
<Formosaic
saveTimeoutMs={15000} // 15 second timeout
maxSaveRetries={5} // Retry up to 5 times with exponential backoff
saveData={async (data) => { /* ... */ }}
/>Built-in accessibility features:
- Focus trap in
ConfirmInputsModal-- Tab key wraps within modal, Escape closes, focus restored on close - Focus-to-first-error on validation failure -- automatically focuses the first field with an error
- ARIA live regions --
<div role="status" aria-live="polite">announces saving/saved/error status to screen readers - aria-label on filter inputs, aria-busy on fields during save
- Wizard step announcements -- screen readers announce "Step 2 of 4: Details" on navigation
Auto-save form state to localStorage for recovery after accidental page closures:
import { useDraftPersistence, useBeforeUnload } from "@formosaic/core";
function MyForm() {
const { isDirty, formValues } = useFormState();
// Auto-save drafts to localStorage every 5 seconds
const { hasDraft, clearDraft } = useDraftPersistence({
formId: "my-form-123",
data: formValues,
saveIntervalMs: 5000,
enabled: isDirty,
storageKeyPrefix: "myApp",
});
// Warn user before leaving page with unsaved changes
useBeforeUnload(isDirty, "You have unsaved changes.");
return <Formosaic /* ... */ />;
}Includes serializeFormState / deserializeFormState utilities for Date-safe JSON round-trips.
Customize field chrome without replacing components:
// Render props on FieldWrapper
<FieldWrapper
renderLabel={(label, required) => <MyCustomLabel text={label} isRequired={required} />}
renderError={(error) => <MyCustomError message={error} />}
renderStatus={(status) => <MyCustomStatus type={status} />}
/>CSS custom properties for global theming (import optional styles.css):
:root {
--formosaic-error-color: #d32f2f;
--formosaic-warning-color: #ed6c02;
--formosaic-saving-color: #0288d1;
--formosaic-label-color: #333;
--formosaic-required-color: #d32f2f;
--formosaic-border-radius: 4px;
--formosaic-field-gap: 12px;
--formosaic-font-size: 14px;
}Form-level error banner via formErrors prop on Formosaic:
<Formosaic
formErrors={["End date must be after start date"]}
/* ... */
/>The headless package renders all 27 field types using native HTML elements with data-field-type and data-field-state attributes for CSS targeting. No UI framework required.
import { createHeadlessFieldRegistry } from "@formosaic/headless";
import "@formosaic/headless/styles.css"; // optional minimal styles
<InjectedFieldProvider injectedFields={createHeadlessFieldRegistry()}>Style with Tailwind CSS, your own stylesheet, or CSS custom properties:
[data-field-type="Textbox"] input {
@apply w-full rounded-md border border-gray-300 px-3 py-2 text-sm
focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200;
}
[data-field-state="error"] input {
@apply border-red-500;
}See the headless package README for full details.
All core components are SSR-safe. Browser-only API access (localStorage, document.activeElement, window.addEventListener) is guarded behind typeof checks or confined to useEffect callbacks.
For Next.js App Router, add "use client" to files containing form components. Server-fetched data can be passed as props across the client boundary.
See the SSR / Next.js integration guide for full setup instructions covering App Router, Pages Router, draft persistence, lazy loading, and common pitfalls.
Migrate from react-jsonschema-form with zero rewrite. Bring your existing schema + uiSchema + formData and get a full IFormConfig with our rules engine layered on top. JSON Schema dependencies and if/then/else are auto-converted to IRule[].
import { fromRjsfSchema } from "@formosaic/core";
// Your existing RJSF schema
const schema = {
type: "object",
properties: {
name: { type: "string", title: "Name", minLength: 1 },
age: { type: "integer", title: "Age", minimum: 0, maximum: 150 },
role: { type: "string", enum: ["admin", "user", "guest"] },
email: { type: "string", format: "email" },
},
required: ["name"],
dependencies: {
role: {
oneOf: [
{
properties: {
role: { const: "admin" },
adminCode: { type: "string", title: "Admin Code" },
},
required: ["adminCode"],
},
],
},
},
};
const uiSchema = {
age: { "ui:widget": "updown" },
email: { "ui:placeholder": "you@example.com" },
"ui:order": ["name", "email", "role", "age", "*"],
};
// Convert to IFormConfig -- dependencies become IRule[] automatically
const formConfig = fromRjsfSchema(schema, uiSchema, existingFormData);
// formConfig.fields.adminCode has rules for conditional visibility based on role
// Use directly with Formosaic
<Formosaic formConfig={formConfig} /* ... */ />Also exports toRjsfSchema(config) for converting back to JSON Schema + uiSchema (best-effort, structural fidelity only).
Convert Zod object schemas to field configs without adding zod as a dependency:
import { zodSchemaToFieldConfig } from "@formosaic/core";
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1),
age: z.number().min(0),
active: z.boolean(),
role: z.enum(["admin", "user", "guest"]),
email: z.string().email(),
startDate: z.date(),
tags: z.array(z.string()),
});
const fieldConfigs = zodSchemaToFieldConfig(UserSchema);
// Maps: ZodString->Textbox, ZodNumber->Number, ZodBoolean->Toggle,
// ZodEnum->Dropdown, ZodDate->DateControl, ZodArray->Multiselect
// Detects .email() and .url() checks for automatic validationNo zod peer dependency is required. If you do not use Zod, this function is tree-shaken out of your bundle.
Load field components on demand using React.lazy for bundle optimization:
import { createLazyFieldRegistry } from "@formosaic/core";
const lazyFields = createLazyFieldRegistry({
Textbox: () => import("./fields/HookTextbox"),
Dropdown: () => import("./fields/HookDropdown"),
// Components are loaded only when first rendered
});
<InjectedFieldProvider injectedFields={lazyFields}>All 27 field types (21 editable + 6 read-only) are available in the Fluent UI, MUI, and headless adapters:
| Component Key | Description |
|---|---|
Textbox |
Single-line text input |
Number |
Numeric input with validation |
Toggle |
Boolean toggle switch |
Dropdown |
Single-select dropdown |
Multiselect |
Multi-select dropdown |
DateControl |
Date picker with clear button |
Slider |
Numeric slider |
| MultiSelectSearch | Searchable multi-select |
| Textarea | Multiline text with expand-to-modal |
| DocumentLinks | URL link CRUD |
| StatusDropdown | Dropdown with color status indicator |
| DynamicFragment | Hidden field (form state only) |
| FieldArray | Repeating section (add/remove items) |
| RadioGroup | Single-select radio button group |
| CheckboxGroup | Multi-select checkbox group (value: string[]) |
| Rating | Star rating input (value: number; configurable max, allowHalf) |
| ColorPicker | Native color picker returning hex string |
| Autocomplete | Searchable single-select with type-ahead |
| FileUpload | File picker (single or multiple); validates size via config.maxSizeMb |
| DateRange | Two date inputs (From / To); value: { start, end } ISO strings |
| DateTime | Combined date+time input; value: ISO datetime-local string |
| PhoneInput | Phone input with inline masking (us, international, raw formats) |
| Component Key | Description |
|---|---|
ReadOnly |
Plain text display |
ReadOnlyArray |
Array of strings |
ReadOnlyDateTime |
Formatted date/time |
ReadOnlyCumulativeNumber |
Computed sum of other fields |
ReadOnlyRichText |
Rendered HTML |
ReadOnlyWithButton |
Text with action button |
Formosaic separates what a form contains (the IFormConfig JSON object) from how it renders (UI adapter packages). The core package owns form state (via react-hook-form), evaluates declarative rules to compute field visibility/required/readOnly state, and delegates rendering to pluggable field components registered through the component injection system. This means you can swap your entire UI layer -- from Fluent UI to MUI to headless HTML -- by changing one import.
<RulesEngineProvider> -- Owns rule state via useReducer (memoized)
<InjectedFieldProvider> -- Component injection registry (memoized)
<Formosaic> -- Form state (react-hook-form), auto-save with retry, rules
<FormFields> -- Renders ordered field list
<FormErrorBoundary> -- Per-field error boundary (crash isolation)
<RenderField> -- Per-field: Controller + component lookup (useMemo)
<FieldWrapper> -- Label, error, saving status (React.memo, render props)
<InjectedField /> -- Your UI component via cloneElement
See the creating an adapter guide for a complete guide. The short version:
- Create field components that accept
IFieldProps<T> - Build a registry mapping
ComponentTypesto your field elements - Pass the registry via the
injectedFieldsprop onInjectedFieldProvider
# Install dependencies
npm install --legacy-peer-deps
# Build all packages
npm run build
# Build individual packages
npm run build:core
npm run build:fluent
npm run build:mui
npm run build:headless
npm run build:antd # Build Ant Design package only
npm run build:chakra # Build Chakra UI package only
npm run build:mantine # Build Mantine package only
npm run build:atlaskit # Build Atlaskit package only
npm run build:base-web # Build Base Web package only
npm run build:heroui # Build HeroUI package only
npm run build:radix # Build Radix package only
npm run build:react-aria # Build React Aria package only
# Run tests
npm run test
npm run test:watch
npm run test:coverage
# Run end-to-end tests
npm run test:e2e
# Run benchmarks
npm run bench
# Storybook
npm run storybook
npm run build-storybook
# Clean build output
npm run cleanpackages/
core/ -- @formosaic/core (React + react-hook-form only)
fluent/ -- @formosaic/fluent (Fluent UI v9 adapter, 27 field types)
mui/ -- @formosaic/mui (Material UI adapter, 27 field types)
headless/ -- @formosaic/headless (semantic HTML adapter, 27 field types)
antd/ -- @formosaic/antd (Ant Design v5 adapter, 27 field types)
chakra/ -- @formosaic/chakra (Chakra UI v3 adapter, 27 field types)
mantine/ -- @formosaic/mantine (Mantine v7 adapter, 27 field types)
atlaskit/ -- @formosaic/atlaskit (Atlassian Design System adapter, 27 field types)
base-web/ -- @formosaic/base-web (Uber Base Web adapter, 27 field types)
heroui/ -- @formosaic/heroui (HeroUI adapter, 27 field types)
radix/ -- @formosaic/radix (Radix UI primitives adapter, 27 field types)
react-aria/ -- @formosaic/react-aria (React Aria Components adapter, 27 field types)
examples/ -- 6 example apps (login+MFA, checkout wizard, data entry, patient-intake, job-application, expense-report)
e2e/ -- Playwright end-to-end tests
benchmarks/ -- Vitest benchmarks for rules engine performance
stories/ -- Storybook stories for field components
docs/ -- MDX documentation (published at https://formosaic.com/) plus internal planning notes (tier1-baseline-report, tier1-patterns, tier2-handoff)
MIT