npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zod-form-engine

v0.0.3

Published

Auto-generate React forms from a Zod schema. Define your fields, validation, and UI in one place — `ZodForm` renders the form for you.

Readme

@omar-ra7al/zod-form

Auto-generate React forms from a Zod schema. Define your fields, validation, and UI in one place — ZodForm renders the form for you.


What this package provides

| Feature | Description | | --------------------------- | ------------------------------------------------------------ | | Schema-driven forms | Build forms from a z.object() Zod schema | | Validation | Powered by Zod + react-hook-form + @hookform/resolvers | | Custom field components | Attach any React input via field() | | Nested objects | Automatically renders grouped sections | | Dynamic arrays | Add/remove repeating items (objects or primitives) | | Container styling | Grid column spans and wrapper classes per field via Tailwind | | Live onChange | Get form values as the user types | | JSON editor | Paste JSON to fill the form, or export current values | | Submit UI | Custom label, icon, styles, and loading state |

Exports: ZodForm (default), field, getFieldMeta.


Requirements

| Package | Version | | --------------------- | --------- | | react | ^19.0.0 | | react-dom | ^19.0.0 | | zod | ^4.0.0 | | react-hook-form | ^7.0.0 | | @hookform/resolvers | ^5.0.0 | | lucide-react | ^1.20.0 |


Installation

npm install @omar-ra7al/zod-form

Quick start

1. Create a field component

Your component receives value, onChange, and anything you pass in props.

type InputProps = {
  value: string;
  onChange: (value: string) => void;
};

function Input({ value, onChange, ...props }: InputProps) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
      className="w-full border p-2 rounded-md"
      {...props}
    />
  );
}

2. Define a Zod schema with field()

Wrap each field with field(zodSchema, meta):

import z from "zod";
import ZodForm, { field } from "@omar-ra7al/zod-form";

const schema = z.object({
  name: field(z.string().min(1, "Name is required"), {
    label: "Name",
    containerClassName: "col-span-1",
    component: Input,
  }),
  email: field(z.email(), {
    label: "Email",
    containerClassName: "col-span-1",
    component: Input,
  }),
});

3. Render the form

function MyForm() {
  return (
    <ZodForm
      zodSchema={schema}
      onSubmit={(data) => console.log(data)}
      mode="onSubmit"
      className="w-full max-w-2xl"
    />
  );
}

How it works

Zod schema + field()  →  ZodForm  →  react-hook-form  →  Your components
       ↓                    ↓
   validation          grid layout
  1. You define a z.object() schema using normal z.object(), z.array(), etc.
  2. Each field that needs UI config is wrapped with field(zodSchema, meta).
  3. field() returns the same Zod schema and stores metadata separately (not on zod.meta()).
  4. ZodForm walks the schema, looks up metadata per field, and renders the matching components.
  5. On submit, validated data is passed to your onSubmit handler.

field() usage rules

  1. Call field() last — after all Zod chaining:
  • field(z.string().min(1), { label: "Name" }) — correct
  • field(z.string(), { label: "Name" }).min(1) — wrong (.min() returns a new instance, meta is lost)
  1. Wrap anything that needs UI config with field():
  • Leaf inputs: field(z.string(), { component, label, ... })
  • Nested sections: field(z.object({ ... }), { label: "Profile", containerClassName })
  • Arrays: field(z.array(...), { label: "Contacts" })
  • Array item cards (optional): field(z.object({...}), { containerClassName })
  1. Don't reuse one schema instance across multiple field() calls (the second call overwrites metadata).

field() meta options

Leaf fields (with component)

| Option | Type | Description | | -------------------- | ---------------------------------- | -------------------------------------------------------------------------------------- | | label | string or { text, className? } | Label shown above the field | | component | React component | Required. Input component to render | | containerClassName | string | Classes on the engine's field wrapper (label + input + error). Default: "col-span-2" | | props | object | Extra props spread onto your component | | defaultValue | any | Fallback when value is empty |

props and defaultValue are fully typed based on your Zod schema output type and component props.

Nested objects

Use field(z.object({ ... }), { label, containerClassName }). Each nested object becomes a section heading with its own grid. There is no depth limit — objects and arrays can nest as deeply as your schema requires.

| Option | Type | Description | | -------------------- | ---------------------------------- | ---------------------------------------------------------------------------- | | label | string or { text, className? } | Section heading | | containerClassName | string | Classes on the section wrapper. Replaces the default section styles when set | | props | object | Extra props spread onto the section wrapper element |

const schema = z.object({
  profile: field(
    z.object({
      name: field(z.string(), { label: "Name", component: Input }),
      address: field(
        z.object({
          street: field(z.string(), { label: "Street", component: Input }),
          location: field(
            z.object({
              city: field(z.string(), { label: "City", component: Input }),
              zip: field(z.string(), { label: "ZIP", component: Input }),
            }),
            { label: "Location" },
          ),
        }),
        { label: "Address" },
      ),
    }),
    {
      label: "Profile",
      containerClassName: "col-span-2 border-l-2 border-primary/20 pl-4 my-2 bg-muted/30",
    },
  ),
});

Dynamic arrays

Use field(z.array(...), { label: "..." }). Users can add/remove items with New item / × buttons.

Array of objects:

const schema = z.object({
  contacts: field(
    z.array(
      z.object({
        phone: field(z.string(), { label: "Phone", component: Input }),
        email: field(z.email(), { label: "Email", component: Input }),
      }),
    ),
    { label: "Contacts" },
  ),
});

Array of primitives (renders a plain text input per item):

const schema = z.object({
  tags: field(z.array(z.string()), { label: "Tags" }),
});

Arrays and objects together:

const schema = z.object({
  projects: field(
    z.array(
      z.object({
        title: field(z.string(), { label: "Title", component: Input }),
        tasks: field(
          z.array(
            z.object({
              name: field(z.string(), { label: "Task", component: Input }),
            }),
          ),
          { label: "Tasks" },
        ),
      }),
    ),
    { label: "Projects" },
  ),
});

Array-specific meta

| Level | Options | | ----------------------------------- | -------------------------------------- | | Array (field(z.array(), {})) | label, containerClassName, props | | Array item (wrapped z.object) | label, containerClassName, props |

Array containerClassName defaults to "grid-cols-2" on the items grid. Array item containerClassName defaults to "col-span-2" on each item card.


ZodForm props

<ZodForm
  zodSchema={schema} // required — z.object() schema
  onSubmit={(data) => {}} // required — called with validated data
  onChange={(data) => {}} // optional — called on every value change
  defaultValues={{}} // optional — initial / reset values
  mode="onChange" // optional — react-hook-form validation mode
  className="" // optional — extra classes on the form wrapper
  jsonConfig={{}} // optional — JSON editor (see below)
  submitConfig={{}} // optional — submit button (see below)
/>

mode

When validation runs. Same as [react-hook-form mode](https://react-hook-form.com/docs/useform#mode):

| Value | When validation runs | | ------------- | -------------------------------------- | | "onChange" | On every change (default) | | "onSubmit" | On submit only | | "onBlur" | On blur | | "onTouched" | After first blur, then on every change | | "all" | On blur and change |

jsonConfig

| Option | Type | Default | Description | | ---------- | --------- | ------- | ----------------------------------------- | | enabled | boolean | false | Show the JSON editor panel | | liveData | boolean | false | Auto-sync editor with current form values |

<ZodForm
  zodSchema={schema}
  onSubmit={handleSubmit}
  jsonConfig={{ enabled: true, liveData: true }}
/>

submitConfig

| Option | Type | Default | Description | | ----------------- | ----------- | ---------------- | ------------------------- | | disabled | boolean | false | Disable the submit button | | submitLabel | string | "Save Changes" | Button text | | submitIcon | ReactNode | save icon | Icon next to the label | | submitClassName | string | built-in styles | Custom button classes |

<ZodForm
  zodSchema={schema}
  onSubmit={handleSubmit}
  submitConfig={{
    submitLabel: "Create account",
    submitClassName: "w-full bg-blue-600 text-white py-2 rounded-lg",
  }}
/>

Custom components

ZodForm always passes **value** and **onChange** to your component.

Same prop names? Use it directly:

name: field(z.string(), {
  label: "Name",
  component: Input,
  props: { placeholder: "Enter your name" },
}),

Use props for extras like placeholder or disabled — not to rename value / onChange.

Different prop names? Wrap the component and map them. Common with shadcn/ui (checked / onCheckedChange, onValueChange, onSelect, etc.):

function FormCheckbox({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) {
  return <Checkbox checked={value} onCheckedChange={onChange} />;
}

Full example

import z from "zod";
import ZodForm, { field } from "@omar-ra7al/zod-form";

const Input = ({ value, onChange, ...props }: any) => (
  <input
    value={value}
    onChange={(e) => onChange(e.target.value)}
    className="w-full border p-2 rounded-md"
    {...props}
  />
);

const schema = z.object({
  name: field(z.string().min(1), {
    label: "Name",
    containerClassName: "col-span-1",
    component: Input,
  }),
  email: field(z.email(), {
    label: "Email",
    containerClassName: "col-span-1",
    component: Input,
  }),
  details: field(
    z.object({
      phone: field(z.string(), { label: "Phone", component: Input }),
    }),
    {
      label: "Contact details",
      containerClassName: "col-span-2 border-l-2 border-primary/20 pl-4 my-2",
    },
  ),
  password: field(z.string().min(8), {
    label: "Password",
    component: Input,
  }),
});

export function SignUpForm() {
  return (
    <ZodForm
      zodSchema={schema}
      mode="onSubmit"
      defaultValues={{ name: "", email: "", password: "" }}
      onChange={(data) => console.log("changed:", data)}
      onSubmit={async (data) => {
        await fetch("/api/signup", {
          method: "POST",
          body: JSON.stringify(data),
        });
      }}
      className="w-full max-w-2xl"
    />
  );
}

TypeScript

Infer form data from your schema:

type FormData = z.infer<typeof schema>;

<ZodForm
  zodSchema={schema}
  onSubmit={(data: FormData) => {
    // data is fully typed
  }}
/>;

field() provides autocomplete for props and defaultValue based on your component and Zod schema types.


License

See LICENSE. Usage as a dependency is permitted. Copying, modifying, or redistributing the source is not.


Links