Default Components
The default component library ships a complete set of pre-built form components so you can render a working form in 2 lines of code.
Use default components when you want a working form fast — prototyping, internal tools, getting started. Use your own components when you need custom design systems, complex UX, or production branding. You can also start with defaults and progressively replace individual components.
Quick Start
npm install @fogpipe/forma-core @fogpipe/forma-react
import { DefaultFormRenderer } from "@fogpipe/forma-react/defaults";
import "@fogpipe/forma-react/defaults/styles.css";
function ContactPage() {
return (
<DefaultFormRenderer
spec={contactFormSpec}
onSubmit={(data) => console.log("Submitted:", data)}
/>
);
}
That's it. You get a fully styled, accessible form with validation, error display, and submit handling.
Override Individual Components
Swap any component while keeping the rest of the defaults:
import { FormRenderer } from "@fogpipe/forma-react";
import {
defaultComponentMap,
defaultFieldWrapper,
defaultLayout,
defaultPageWrapper,
} from "@fogpipe/forma-react/defaults";
import "@fogpipe/forma-react/defaults/styles.css";
// Replace just the select component
const components = { ...defaultComponentMap, select: MyFancySelect };
function CustomForm() {
return (
<FormRenderer
spec={formSpec}
components={components}
fieldWrapper={defaultFieldWrapper}
layout={defaultLayout}
pageWrapper={defaultPageWrapper}
onSubmit={handleSubmit}
/>
);
}
You can also import individual components for cherry-picking:
import {
TextInput,
SelectInput,
ArrayField,
} from "@fogpipe/forma-react/defaults";
Wizard Forms
For multi-page forms, set the wizardLayout prop:
<DefaultFormRenderer
spec={multiPageSpec}
onSubmit={handleSubmit}
wizardLayout
/>
This gives you:
- Step indicator with completed/current/upcoming states
- Previous/Next/Submit buttons — Next and Submit are always separate (prevents accidental submission)
- Page validation — Next button validates current page fields before advancing
- Keyboard handling — Enter key on non-last pages triggers Next, not Submit
Programmatic Control
Forward a ref for imperative API access:
import { useRef } from "react";
import { DefaultFormRenderer } from "@fogpipe/forma-react/defaults";
import type { FormRendererHandle } from "@fogpipe/forma-react";
function ProgrammaticForm() {
const formRef = useRef<FormRendererHandle>(null);
return (
<>
<DefaultFormRenderer ref={formRef} spec={spec} onSubmit={handleSubmit} />
<button onClick={() => formRef.current?.submitForm()}>
External Submit
</button>
<button onClick={() => formRef.current?.resetForm()}>Reset</button>
</>
);
}
Theming
All styling is driven by CSS variables. Override them to match your brand:
:root {
--forma-color-primary: #7c3aed;
--forma-color-primary-hover: #6d28d9;
--forma-color-error: #e11d48;
--forma-radius: 12px;
--forma-font-family: "Inter", sans-serif;
}
Dark Mode
Dark mode works automatically via prefers-color-scheme, or manually with the data-theme attribute:
<!-- Automatic: follows system preference -->
<div>
<DefaultFormRenderer spec="{spec}" onSubmit="{handleSubmit}" />
</div>
<!-- Manual: force dark mode -->
<div data-theme="dark">
<DefaultFormRenderer spec="{spec}" onSubmit="{handleSubmit}" />
</div>
<!-- Manual: force light mode (overrides system dark preference) -->
<div data-theme="light">
<DefaultFormRenderer spec="{spec}" onSubmit="{handleSubmit}" />
</div>
CSS Variable Reference
Semantic Tokens
These are the primary variables consumers should override:
| Variable | Default | Description |
|---|---|---|
--forma-font-family | system-ui, -apple-system, sans-serif | Font stack |
--forma-font-size | 16px | Base font size |
--forma-line-height | 1.5 | Base line height |
--forma-color-primary | #2563eb | Primary/accent color |
--forma-color-primary-hover | #1d4ed8 | Primary hover state |
--forma-color-error | #dc2626 | Error border/accent |
--forma-color-warning | #d97706 | Warning border/accent |
--forma-color-border | #d1d5db | Input border color |
--forma-color-border-focus | #2563eb | Focused input border |
--forma-color-bg | #ffffff | Background color |
--forma-color-bg-disabled | #f9fafb | Disabled field background |
--forma-color-text | #111827 | Primary text color |
--forma-color-text-muted | #6b7280 | Secondary/help text |
--forma-color-text-error | #dc2626 | Error message text |
--forma-color-text-warning | #d97706 | Warning message text |
--forma-radius | 6px | Border radius |
--forma-spacing | 16px | Base spacing unit |
--forma-spacing-sm | 8px | Small spacing |
--forma-spacing-xs | 4px | Extra-small spacing |
--forma-transition | 150ms ease | Transition timing |
Component Tokens
These reference semantic tokens but can be overridden independently:
| Variable | Default | Description |
|---|---|---|
--forma-input-bg | var(--forma-color-bg) | Input background |
--forma-input-border | var(--forma-color-border) | Input border |
--forma-input-border-focus | var(--forma-color-border-focus) | Input focus border |
--forma-input-radius | var(--forma-radius) | Input border radius |
--forma-input-padding | 0.5rem 0.75rem | Input padding |
--forma-input-font-size | var(--forma-font-size) | Input font size |
--forma-label-font-size | 0.875rem | Label font size |
--forma-label-font-weight | 500 | Label font weight |
--forma-label-color | var(--forma-color-text) | Label text color |
--forma-focus-ring-color | var(--forma-color-border-focus) | Focus ring color |
--forma-focus-ring-width | 2px | Focus ring width |
--forma-error-font-size | 0.8125rem | Error text size |
--forma-error-color | var(--forma-color-text-error) | Error text color |
--forma-warning-color | var(--forma-color-text-warning) | Warning text color |
--forma-button-bg | var(--forma-color-primary) | Button background |
--forma-button-bg-hover | var(--forma-color-primary-hover) | Button hover |
--forma-button-color | #ffffff | Button text color |
--forma-button-radius | var(--forma-radius) | Button border radius |
--forma-button-padding | 0.625rem 1.25rem | Button padding |
Component Reference
| Field Type | Component | Renders |
|---|---|---|
text, email, phone, url, password | TextInput | <input> with type mapping (phone → tel) |
textarea | TextareaInput | <textarea> with 3 rows |
number | NumberInput | <input type="number"> — empty → null, NaN → null |
integer | IntegerInput | <input type="number" step="1"> — uses parseInt |
boolean | BooleanInput | Custom styled checkbox with inline label |
date | DateInput | <input type="date"> — native date picker |
datetime | DateTimeInput | <input type="datetime-local"> |
select | SelectInput | Native <select> with custom arrow, placeholder option |
multiselect | MultiSelectInput | <fieldset> with checkbox per option |
array | ArrayField | Item list with Add/Remove buttons, stable keys |
object | ObjectField | <fieldset> with <legend> |
computed | ComputedDisplay | Read-only <output> element |
display | DisplayField | Static text content |
matrix | MatrixField | <table> with radio buttons (single) or checkboxes (multi) |
fallback | FallbackField | Text input + dev-mode warning for unknown types |
Layout Components
| Component | Description |
|---|---|
FieldWrapper | Renders label, description, errors (touched-gated), required indicator. Skips label for boolean fields (they render their own). |
FormLayout | <form> with submit button. Submit is never disabled when invalid — clicking triggers validation so users can see what's wrong. |
WizardLayout | Multi-page layout with step indicator, Previous/Next/Submit navigation, page validation. Falls back to FormLayout for single-page specs. |
PageWrapper | Page title (<h2>) and description. |
Built-in Gotcha Prevention
The default components solve common pitfalls automatically:
| Problem | Solution |
|---|---|
| Error messages shown before user interacts | FieldWrapper gates errors on touched state — errors appear only after blur or form submission |
| Page refresh on form submit | FormLayout calls e.preventDefault() |
| Wizard submit on Enter key | WizardLayout uses separate Next/Submit buttons with submitter detection |
| Number input returns string | NumberInput parses with parseFloat(), empty → null, NaN → null |
| Required indicator on boolean fields | Hidden unless field has explicit validation rules (consent checkbox pattern) |
| Array item key instability | ArrayField uses ref-based sequential counter for stable React keys |
Exports
Everything is available from @fogpipe/forma-react/defaults:
// Convenience wrapper
import { DefaultFormRenderer } from "@fogpipe/forma-react/defaults";
// Component map and layout aliases
import {
defaultComponentMap,
defaultFieldWrapper,
defaultLayout,
defaultWizardLayout,
defaultPageWrapper,
} from "@fogpipe/forma-react/defaults";
// Individual components (for cherry-picking)
import {
TextInput,
TextareaInput,
NumberInput,
IntegerInput,
BooleanInput,
DateInput,
DateTimeInput,
SelectInput,
MultiSelectInput,
ArrayField,
ObjectField,
ComputedDisplay,
DisplayField,
MatrixField,
FallbackField,
} from "@fogpipe/forma-react/defaults";
// Layout components
import {
FieldWrapper,
FormLayout,
WizardLayout,
PageWrapper,
} from "@fogpipe/forma-react/defaults";
// CSS (import once, typically in your app root)
import "@fogpipe/forma-react/defaults/styles.css";
Next Steps
- Component Mapping — Build custom components for full control
- Styling Guide — Tailwind/CSS Modules patterns for custom components
- Wizard Forms — Deep dive into multi-page form configuration
- Events — React to form lifecycle events (analytics, data injection)