Releases: seasonedcc/remix-forms
v5.0.0-alpha.9
New Features
First-class array and object field support (#418, closes #74)
Until now, SchemaForm only knew about scalar fields — strings, numbers, booleans, dates, and files. Array and object fields in your schema were ignored, requiring manual wiring with hidden inputs and setValue.
This release teaches SchemaForm to understand the full depth of your schema. Scalar arrays, object arrays, arrays of arrays, and deeply nested objects all auto-render out of the box:
<SchemaForm schema={schema} />The polymorphic Field component detects each field's schema type and generates the appropriate UI automatically, with unlimited recursive depth.
Children render functions adapt per field type. Array fields expose Item, items, append, remove, move, swap, AddButton, RemoveButton, ArrayEmpty, ScalarArrayField, Title, and Errors. Object fields expose a scoped Field, ObjectFields, Title, and Errors. Components received in children functions are automatically enhanced via mapChildren — labels, ids, ARIA attributes, and content are injected without manual prop passing.
Item children are optional — omitting them auto-renders the item content plus a RemoveButton, wrapped in the appropriate type-specific slot component (scalarArrayItem, objectArrayItem, or arrayArrayItem).
Array and object headings use Title (not Label) backed by dedicated arrayTitle/objectTitle slots that default to <div> — avoiding a11y warnings from labels not associated with form controls.
Auto-focus — the Add button automatically focuses the first input of the newly added item across all array types.
No hardcoded strings — all user-facing copy is configurable via props: emptyArrayLabel (default: 'No items'), addButtonLabel (default: 'Add'), removeButtonLabel (default: 'Remove').
Nested enum fields automatically render as <select> with options inferred from the schema.
6 type-specific render functions (#418)
With SchemaForm now rendering three categories of fields (scalar, array, object) and three categories of array items (scalar, object, nested array), customizing rendering requires more granularity than a single renderField could provide.
The old renderField prop has been renamed to renderScalarField — it works the same way, just with a more precise name. Five new render functions join it for the new field and item types:
Field-level (wrap <Field>):
renderScalarField— the oldrenderField, for string, number, boolean, date, and file fieldsrenderArrayField— customize how array fields render (e.g., wrap in a<section>with a heading)renderObjectField— customize how object fields render (e.g., wrap in a<fieldset>with a<legend>)
Item-level (wrap <Item> inside arrays):
renderScalarArrayItem— customize scalar items in arraysrenderObjectArrayItem— customize object items in arrays (e.g., add drag-and-drop handles)renderArrayArrayItem— customize nested array items in arrays
Each receives narrowed fieldType and shape props with full TypeScript safety. All follow a 3-level fallback: per-form prop → makeSchemaForm factory → default.
create-remix-forms CLI (#420, closes #419)
New npx create-remix-forms command that scaffolds styled components for makeSchemaForm. Ships with two presets:
- Tailwind CSS — plain utility classes
- DaisyUI — Tailwind + DaisyUI component classes
Generates 27 individual component files + barrel index.ts in a configurable output directory. Supports interactive prompts and CI-friendly flags (--preset, --output, -y). Auto-detects your package manager and installs remix-forms and tailwind-merge.
Type safety improvements (#418)
Replaced 58 noExplicitAny biome-ignores with proper types:
ComponentSlotsconstrains resolved components to the 26 known slot keys —Resolved['typo']is now a compile errorArrayFieldInnerProps/ObjectFieldInnerPropsuse proper interfaces instead ofRecord<string, any>ArraySchemaInfo/ObjectSchemaInfonarrowed from genericSchemaInfofor type-safe.itemand.fieldsaccessUseFormRegister<Infer<Schema>>— schema-aware register type increateField
Breaking Changes
Peer dependency bumps
Two peer dependencies now require higher minimum versions:
schema-info—>=0.4.1→>=0.5.0(adds array/object schema introspection)coerce-form-data—>=2.1→>=3.0.0(adds nested field coercion)
renderField renamed to renderScalarField
The renderField prop on SchemaForm and makeSchemaForm has been renamed to renderScalarField. The function signature is unchanged — just update the prop name.
Exported types renamed
RenderFieldProps→RenderScalarFieldPropsRenderField→RenderScalarField
FormValues type narrowed
FormValues<T> values changed from any to unknown. Consumers accessing .values.field on MutationResult must narrow the type first.
ComponentMap expanded
10 new required slots for array/object support: scalarArrayField, scalarArrayItem, objectArrayItem, arrayArrayItem, addButton, removeButton, arrayEmpty, objectFields, arrayTitle, objectTitle. Consumers passing a full custom ComponentMap to makeSchemaForm must include these new slots (all have sensible defaults when omitted).
What's Changed
- Add first-class array and object field support by @danielweinmann in #418
- Add create-remix-forms CLI by @danielweinmann in #420
Full Changelog: v5.0.0-alpha.8...v5.0.0-alpha.9
v5.0.0-alpha.8
Breaking Changes
Checkbox and radio inputs now nest inside labels (#408)
Checkbox and radio <input> elements are now rendered inside their <label>, following MDN best practice. This eliminates click "blind areas" between inputs and their label text.
The checkboxWrapper and radioWrapper component slots have been removed (16 → 14 slots), as the label now serves as the wrapper.
Before:
<div class="wrapper">
<input type="checkbox" id="agree" />
<label for="agree">I agree</label>
</div>After:
<label>
<input type="checkbox" />
I agree
</label>Field now accepts SmartInput props directly; wrapper props move to fieldProps (#411)
Field accepts type-safe props from the inferred SmartInput slot directly — no children render prop needed for simple cases:
// Before — children render prop just to pass rows:
<Field name="bio" multiline>
{({ SmartInput }) => <SmartInput rows={5} />}
</Field>
// After — rows goes directly on Field:
<Field name="bio" multiline rows={5} />Wrapper customization (e.g. className, style) moves to an explicit fieldProps prop:
<Field name="bio" multiline rows={5} fieldProps={{ className: "wrapper" }} />New peer dependency: @remix-run/form-data-parser (#417)
File upload support introduces a new peer dependency. Install it alongside remix-forms:
pnpm add @remix-run/form-data-parserBumped peer dependencies (#417)
coerce-form-databumped from>=2to>=2.1schema-infobumped from>=0.3.0to>=0.4.1
New Features
- First-class file upload support (#417) —
z.instanceof(File)auto-renders<input type="file">, auto-setsencType="multipart/form-data", validates file size/type on the client, and integrates with@remix-run/form-data-parseron the server via a newuploadHandleroption. NewfileInputcomponent slot andacceptform-level prop. Fieldscomponent (#412) — Automatic field rendering inside custom layouts. Use<Fields />without children for the default layout, or with<Field>children as overrides.renderFormprop (#413) — Customize the default form layout when nochildrenare provided, following the same pattern asrenderField. Also available as a factory option viamakeSchemaForm.renderFieldinmakeSchemaFormoptions (#415) — Set a globalrenderFieldat the factory level; per-formrenderFieldoverrides it.- Auto-render hidden fields with custom children (#416) — Hidden fields declared via
hiddenFieldsare now automatically rendered even when using customchildrenwithout<Fields />. checkboxLabelandradioLabelcomponent slots (#407) — Independent styling of checkbox/radio option labels vs the field-levellabel.- Schema-level
autoCompleteprop (#403) — Per-fieldautoCompleteattribute, same pattern aslabelsandplaceholders. - Field wrapper props inferred from component slot (#409) —
FieldPropsnow infers wrapper props from the resolvedfieldcomponent instead of hardcoding div props.
Bug Fixes
- Fix button
name/valuenot serialized on client-side submit (#404) — Closes #158 - Strip
defaultValue/defaultCheckedfrom user-facing children components (#405) — Closes #168 - Pass
type="hidden"to inputs when Field is hidden (#406) — Closes #227 - Forward all
makeFieldprops to Field with custom children (#410)
What's Changed
- Add schema-level autoComplete prop to SchemaForm by @danielweinmann in #403
- Fix button name/value not serialized on client-side submit by @danielweinmann in #404
- Strip defaultValue/defaultChecked from user-facing children components by @danielweinmann in #405
- Pass type="hidden" to inputs when Field is hidden by @danielweinmann in #406
- Add checkboxLabel and radioLabel component slots by @danielweinmann in #407
- BREAKING: Nest checkbox and radio inputs inside labels by @danielweinmann in #408
- Infer Field wrapper props from the field component slot by @danielweinmann in #409
- Forward all makeField props to Field with custom children by @danielweinmann in #410
- BREAKING: Add inferred props to Field, move wrapper props to fieldProps by @danielweinmann in #411
- Add Fields component for automatic field rendering in custom layouts by @danielweinmann in #412
- Add renderForm prop to SchemaForm by @danielweinmann in #413
- Add renderField to makeSchemaForm options by @danielweinmann in #415
- Auto-render hidden fields with custom children by @danielweinmann in #416
- Add first-class file upload support by @danielweinmann in #417
Full Changelog: v5.0.0-alpha.7...v5.0.0-alpha.8
v5.0.0-alpha.7
Breaking Changes
Replaced individual component props with a unified components prop
The 15 individual component props on SchemaForm (inputComponent, labelComponent, multilineComponent, selectComponent, checkboxComponent, radioComponent, checkboxWrapperComponent, radioWrapperComponent, radioGroupComponent, fieldErrorsComponent, fieldComponent, fieldsComponent, globalErrorsComponent, buttonComponent, and component) have been replaced by a single components prop that accepts a partial ComponentMap.
Before:
<SchemaForm
schema={schema}
component={StyledForm}
inputComponent={MyInput}
buttonComponent={MyButton}
labelComponent={MyLabel}
fieldsComponent={MyFieldsWrapper}
globalErrorsComponent={MyErrors}
/>After:
<SchemaForm
schema={schema}
components={{
form: StyledForm,
input: MyInput,
button: MyButton,
label: MyLabel,
fields: MyFieldsWrapper,
globalErrors: MyErrors,
}}
/>Every slot in ComponentMap is now constrained to a component that accepts the minimum props the library passes at runtime. Components with incompatible props fail at compile time instead of silently breaking at runtime.
The full set of slots is: form, field, label, input, multiline, select, checkbox, radio, checkboxWrapper, radioWrapper, radioGroup, fieldErrors, error, fields, globalErrors, button.
New makeSchemaForm factory
makeSchemaForm sets base components once for your entire app. The factory captures concrete types so that custom component props flow through to Field children with full type safety:
import { makeSchemaForm } from 'remix-forms'
const SchemaForm = makeSchemaForm({
form: StyledForm,
input: ChakraInput,
multiline: ChakraTextarea,
button: ChakraButton,
})The returned component still accepts a components prop for per-form overrides. Components resolve in a 3-level cascade: per-form override > base (from makeSchemaForm) > built-in defaults.
Type-safe SmartInput
SmartInput now automatically knows which component it will render — derived from three sources — and accepts only that component's props:
- Schema type —
boolean→ Checkbox, enum → Select - SchemaForm config —
multiline={['bio']},radio={['role']}(usesconsttype params for literal tuple inference) - Field-level props —
<Field multiline>,<Field radio>
<SchemaForm schema={schema} multiline={['bio']} radio={['role']}>
{({ Field }) => (
<>
{/* SmartInput infers Input slot → accepts ChakraInput's size/variant */}
<Field name="email">
{({ SmartInput }) => <SmartInput variant="filled" />}
</Field>
{/* 'bio' is in multiline config → SmartInput infers Multiline slot */}
<Field name="bio">
{({ SmartInput }) => <SmartInput resize="none" />}
</Field>
</>
)}
</SchemaForm>Bug fix
Fixed a pre-existing bug where the radio prop wasn't passed to Field via cloneElement when using custom children layouts, causing radio fields to never render as radio buttons in that scenario.
New Exports
makeSchemaForm— factory function for creating pre-configuredSchemaFormcomponentsComponentMap(type) — describes all 16 component slots available for customisationMergeComponents(type) — utility type for the 3-level component cascade
What's Changed
- BREAKING: Replace individual component props with generic components prop by @danielweinmann in #402
Full Changelog: v5.0.0-alpha.6...v5.0.0-alpha.7
v5.0.0-alpha.6
Breaking Changes
React 18+ now required (#400)
The minimum React version is now 18 (previously 16.8). This is because form field IDs are now generated with useId() — a React 18 hook — to prevent ID collisions when multiple forms share the same schema on a page.
If you're already on React 18 or 19, no changes are needed.
schema-info >=0.3.0 now required
The minimum schema-info peer dependency is now 0.3.0 (previously 0.1.0).
What's Changed
- Use useId for unique form field IDs by @danielweinmann in #400
- Add autoInputTypes prop for format-based input type detection by @danielweinmann in #401
Full Changelog: v5.0.0-alpha.5...v5.0.0-alpha.6
v5.0.0-alpha.5
What's Changed
- Flatten nested errors to dot-path keys by @danielweinmann in #398
- Fix false-positive element type comparisons by @danielweinmann in #399
Full Changelog: v5.0.0-alpha.4...v5.0.0-alpha.5
v5.0.0-alpha.4
Breaking Changes
Multi-library schema support (replaces Zod-only)
remix-forms no longer depends on Zod. It now supports 6 validation libraries — Zod, Valibot, ArkType, Effect Schema, Yup, and Joi — powered by schema-info for schema introspection and @hookform/resolvers/standard-schema for validation.
Removed exports
zodResolver— validation now uses@hookform/resolvers/standard-schemainternally; no resolver export is needed.objectFromSchema/ObjectFromSchema— replaced by schema-info.Resolvertype — no longer part of the public API.
New peer dependencies
schema-info(>=0.1.0) — universal schema introspection for the 6 supported validation libraries.coerce-form-data(>=2) — zero-dependency form data coercion using web standard APIs.
Removed peer dependency
zodis no longer required. Install only the validation library you use.
What's Changed
- Extract coerce-form-data as a zero-dependency package by @danielweinmann in #390
- Migrate from npm to pnpm by @danielweinmann in #391
- Upgrade coerce-form-data to v2.0.0 by @danielweinmann in #392
- Use schema-info for schema introspection by @danielweinmann in #393
- Switch to Standard Schema, remove Zod dependency by @danielweinmann in #394
- Upgrade website to Tailwind CSS v4 by @danielweinmann in #395
- Migrate website to DaisyUI by @danielweinmann in #396
- Redesign website with dark red theme by @danielweinmann in #397
Full Changelog: v5.0.0-alpha.3...v5.0.0-alpha.4
v5.0.0-alpha.3
What's Changed
- Breaking: Coerce 'false' to false and 'null' to null for boolean schemas by @danielweinmann in #386
- Add fieldsComponent prop to SchemaForm by @danielweinmann in #388
- Update react-router from 7.9.4 to 7.13.1
Full Changelog: v4.0.0...v5.0.0-alpha.3
v4.0.0
Breaking changes
1. Only Zod v4 is supported
Remix Forms v4 removed support for Zod v3. Before upgrading Remix Forms, please upgrade your Zod dependency to v4.
If you cannot upgrade to Zod v4 yet, Remix Forms 3.1.1 is stable and will continue working with Zod v3.
How to upgrade
Update your Zod dependency to v4:
npm install zod@latestMost Remix Forms users won't need to change their code after upgrading Zod. However, if you use advanced Zod features, you may need to make some adjustments. The most common breaking changes in Zod v4 that may affect your schemas are:
Default behavior in optional fields
Zod v4 now applies defaults inside properties even within optional fields. This change aligns better with expectations but may cause breakage if your code relies on key existence.
Before (Zod v3):
const schema = z.object({
name: z.string(),
role: z.string().default('user').optional(),
})
schema.parse({ name: 'Alice' })
// Result: { name: 'Alice' }After (Zod v4):
const schema = z.object({
name: z.string(),
role: z.string().default('user').optional(),
})
schema.parse({ name: 'Alice' })
// Result: { name: 'Alice', role: 'user' }Object methods replaced with top-level functions
The .strict() and .passthrough() methods have been replaced with top-level functions.
Before (Zod v3):
const schema = z.object({ name: z.string() }).strict()
const schema2 = z.object({ name: z.string() }).passthrough()After (Zod v4):
const schema = z.strictObject({ name: z.string() })
const schema2 = z.looseObject({ name: z.string() })Function API changes
If you use z.function(), the API has changed significantly. The result is no longer a Zod schema but a "function factory" for defining Zod-validated functions.
Before (Zod v3):
const myFunction = z.function()
.args(z.string(), z.number())
.returns(z.boolean())After (Zod v4):
const myFunction = z.function(
z.tuple([z.string(), z.number()]),
z.boolean(),
)Array .nonempty() behavior
The .nonempty() method now behaves identically to .min(1). The inferred type does not change, but if you relied on the old behavior for type narrowing, consider using z.tuple() instead.
For a complete list of Zod v4 breaking changes, see the official Zod v4 migration guide.
Minor changes
2. New exports: objectFromSchema and ObjectFromSchema
We now export two new utilities that were previously internal:
objectFromSchema: A function that creates a default object from a Zod schemaObjectFromSchema: A TypeScript type that infers the object type from a schema
These can be useful for creating initial form values:
import { objectFromSchema, type ObjectFromSchema } from 'remix-forms'
import { z } from 'zod'
const schema = z.object({
name: z.string().default(''),
age: z.number().default(0),
})
const initialValues = objectFromSchema(schema)
// Result: { name: '', age: 0 }
type FormData = ObjectFromSchema<typeof schema>
// Type: { name: string; age: number }What's Changed
- Add missing shapeInfo enum test by @danielweinmann in #354
- Add missing mutation error test by @danielweinmann in #356
- Add missing boolean coercion and navigation tests by @danielweinmann in #355
- Add tests for mutation transformResult hooks by @danielweinmann in #357
- Fix flaky zod-effects test by @danielweinmann in #359
- Fix e2e test flakiness by @danielweinmann in #360
- Fix flaky test by mocking router early by @danielweinmann in #361
- Split long e2e tests by @danielweinmann in #362
- Split slow tests into separate files by @danielweinmann in #363
- Split slow tests by @danielweinmann in #365
- Refactor coerceToForm by @danielweinmann in #366
- Split slow e2e tests by @danielweinmann in #367
- Split some e2e specs for parallel test runs by @danielweinmann in #374
- Add Claude instructions by @felipefreitag in #379
- .github/workflows: Migrate workflows to Blacksmith runners by @blacksmith-sh[bot] in #382
- [BREAKING CHANGE] Support Zod4 and drop Zod3 support by @danielweinmann in #383
- Upgrade React Router to 7.9.4 and export objectFromSchema and ObjectFromSchema by @danielweinmann in #384
New Contributors
- @blacksmith-sh[bot] made their first contribution in #382
Full Changelog: v3.1.1...v4.0.0
v3.1.1
What's Changed
- Fix accented labels in selects by @danielweinmann in #351
Full Changelog: v3.1.0...v3.1.1
v3.1.0
What's Changed
- Add autoComplete prop to Field by @danielweinmann in #344
Full Changelog: v3.0.1...v3.1.0