Skip to content

Releases: seasonedcc/remix-forms

v5.0.0-alpha.9

15 Apr 03:02

Choose a tag to compare

v5.0.0-alpha.9 Pre-release
Pre-release

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 old renderField, for string, number, boolean, date, and file fields
  • renderArrayField — 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 arrays
  • renderObjectArrayItem — 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:

  • ComponentSlots constrains resolved components to the 26 known slot keys — Resolved['typo'] is now a compile error
  • ArrayFieldInnerProps / ObjectFieldInnerProps use proper interfaces instead of Record<string, any>
  • ArraySchemaInfo / ObjectSchemaInfo narrowed from generic SchemaInfo for type-safe .item and .fields access
  • UseFormRegister<Infer<Schema>> — schema-aware register type in createField

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

  • RenderFieldPropsRenderScalarFieldProps
  • RenderFieldRenderScalarField

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

Full Changelog: v5.0.0-alpha.8...v5.0.0-alpha.9

v5.0.0-alpha.8

26 Mar 18:27

Choose a tag to compare

v5.0.0-alpha.8 Pre-release
Pre-release

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-parser

Bumped peer dependencies (#417)

  • coerce-form-data bumped from >=2 to >=2.1
  • schema-info bumped from >=0.3.0 to >=0.4.1

New Features

  • First-class file upload support (#417) — z.instanceof(File) auto-renders <input type="file">, auto-sets encType="multipart/form-data", validates file size/type on the client, and integrates with @remix-run/form-data-parser on the server via a new uploadHandler option. New fileInput component slot and accept form-level prop.
  • Fields component (#412) — Automatic field rendering inside custom layouts. Use <Fields /> without children for the default layout, or with <Field> children as overrides.
  • renderForm prop (#413) — Customize the default form layout when no children are provided, following the same pattern as renderField. Also available as a factory option via makeSchemaForm.
  • renderField in makeSchemaForm options (#415) — Set a global renderField at the factory level; per-form renderField overrides it.
  • Auto-render hidden fields with custom children (#416) — Hidden fields declared via hiddenFields are now automatically rendered even when using custom children without <Fields />.
  • checkboxLabel and radioLabel component slots (#407) — Independent styling of checkbox/radio option labels vs the field-level label.
  • Schema-level autoComplete prop (#403) — Per-field autoComplete attribute, same pattern as labels and placeholders.
  • Field wrapper props inferred from component slot (#409) — FieldProps now infers wrapper props from the resolved field component instead of hardcoding div props.

Bug Fixes

  • Fix button name/value not serialized on client-side submit (#404) — Closes #158
  • Strip defaultValue/defaultChecked from user-facing children components (#405) — Closes #168
  • Pass type="hidden" to inputs when Field is hidden (#406) — Closes #227
  • Forward all makeField props to Field with custom children (#410)

What's Changed

Full Changelog: v5.0.0-alpha.7...v5.0.0-alpha.8

v5.0.0-alpha.7

24 Mar 03:24

Choose a tag to compare

v5.0.0-alpha.7 Pre-release
Pre-release

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:

  1. Schema typeboolean → Checkbox, enum → Select
  2. SchemaForm configmultiline={['bio']}, radio={['role']} (uses const type params for literal tuple inference)
  3. 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-configured SchemaForm components
  • ComponentMap (type) — describes all 16 component slots available for customisation
  • MergeComponents (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

23 Mar 20:06

Choose a tag to compare

v5.0.0-alpha.6 Pre-release
Pre-release

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

Full Changelog: v5.0.0-alpha.5...v5.0.0-alpha.6

v5.0.0-alpha.5

21 Mar 12:32

Choose a tag to compare

v5.0.0-alpha.5 Pre-release
Pre-release

What's Changed

Full Changelog: v5.0.0-alpha.4...v5.0.0-alpha.5

v5.0.0-alpha.4

20 Mar 20:41

Choose a tag to compare

v5.0.0-alpha.4 Pre-release
Pre-release

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-schema internally; no resolver export is needed.
  • objectFromSchema / ObjectFromSchema — replaced by schema-info.
  • Resolver type — 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

  • zod is no longer required. Install only the validation library you use.

What's Changed

Full Changelog: v5.0.0-alpha.3...v5.0.0-alpha.4

v5.0.0-alpha.3

18 Mar 17:41

Choose a tag to compare

v5.0.0-alpha.3 Pre-release
Pre-release

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

23 Oct 19:44

Choose a tag to compare

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@latest

Most 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 schema
  • ObjectFromSchema: 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

New Contributors

Full Changelog: v3.1.1...v4.0.0

v3.1.1

23 May 13:28

Choose a tag to compare

What's Changed

Full Changelog: v3.1.0...v3.1.1

v3.1.0

22 May 10:34

Choose a tag to compare

What's Changed

Full Changelog: v3.0.1...v3.1.0