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

@snowpact/react-rhf-zod-form

v1.4.8

Published

Automatic form generation from Zod schemas with react-hook-form

Readme

@snowpact/react-rhf-zod-form

Automatic form generation from Zod schemas with react-hook-form.

Live Demo

Requirements

  • React >= 18.0.0
  • react-hook-form >= 7.0.0
  • zod >= 4.0.0
  • @hookform/resolvers >= 3.0.0

Features

  • Zero runtime dependencies - Only peer dependencies (React, Zod, RHF)
  • Automatic field type detection - Maps Zod types to form inputs
  • Schema refinements - Full support for refine() and superRefine() cross-field validation
  • Extensible component registry - Use your own components (inputs, layout, submit button)
  • Translation support - i18next, next-intl, or any translation function
  • Children pattern - Full control over layout when needed
  • TypeScript first - Full type inference from Zod schemas

Installation

npm install @snowpact/react-rhf-zod-form

Peer Dependencies

npm install react-hook-form zod @hookform/resolvers

Quick Start

1. Setup (run once at app startup)

Register your components once at app startup (e.g., app/setup.ts, _app.tsx, or main.tsx).

import { setupSnowForm } from '@snowpact/react-rhf-zod-form';
import type { RegisteredComponentProps, FormUILabelProps } from '@snowpact/react-rhf-zod-form';

// Example custom input component
function MyInput({ value, onChange, placeholder, disabled, className, name }: RegisteredComponentProps<string>) {
  return (
    <input
      id={name}
      name={name}
      type="text"
      value={value ?? ''}
      onChange={(e) => onChange(e.target.value)}
      placeholder={placeholder}
      disabled={disabled}
      className={`my-input ${className ?? ''}`}
    />
  );
}

// Example custom label component
function MyLabel({ children, required, invalid, htmlFor }: FormUILabelProps) {
  return (
    <label htmlFor={htmlFor} className={`my-label ${invalid ? 'my-label-error' : ''}`}>
      {children}
      {required && <span className="text-red-500 ml-1">*</span>}
    </label>
  );
}

setupSnowForm({
  translate: (key) => key,
  // Essential components (a warning is logged if any are missing)
  components: {
    text: MyInput,
    email: (props) => <MyInput {...props} type="email" />,
    password: (props) => <MyInput {...props} type="password" />,
    textarea: MyTextarea,
    select: MySelect,
    checkbox: MyCheckbox,
    number: MyNumberInput,
    date: MyDatePicker,
    // Optional: radio, time, datetime-local, tel, url, color, file
  },
  formUI: {
    label: MyLabel,
    description: ({ children }) => <p className="my-description">{children}</p>,
    errorMessage: ({ message }) => <p className="my-error">{message}</p>,
  },
  submitButton: ({ loading, disabled, children }) => (
    <button type="submit" disabled={disabled || loading} className="my-button">
      {loading ? 'Loading...' : children}
    </button>
  ),
  styles: {
    form: 'space-y-4',              // Applied to <form>
    formItem: 'grid gap-2',         // Applied to each field wrapper
    label: 'text-sm font-medium',   // Applied to labels
    description: 'text-xs text-gray-500', // Applied to descriptions
    errorMessage: 'text-xs text-red-500', // Applied to error messages
  },
});

2. Use SnowForm

import { SnowForm } from '@snowpact/react-rhf-zod-form';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(5),
});

function MyForm() {
  return (
    <SnowForm
      schema={schema}
      onSubmit={async (values) => {
        console.log(values); // { name: string, email: string, age: number }
      }}
      onSuccess={() => {
        alert('Form submitted!');
      }}
    />
  );
}

Field Type Mapping

Automatic Detection (from Zod schema)

| Zod Type | Default Component | | -------------------- | ----------------- | | z.string() | Text input | | z.string().email() | Email input | | z.number() | Number input | | z.boolean() | Checkbox | | z.date() | Date picker | | z.enum([...]) | Select |

Available Built-in Types (via overrides)

| Type | Description | | ---------------- | ----------------------------------- | | text | Standard text input | | email | Email input with validation styling | | password | Password input (masked) | | number | Numeric input | | textarea | Multi-line text area | | select | Dropdown select | | checkbox | Boolean checkbox | | radio | Radio button group | | date | Date picker | | time | Time picker | | datetime-local | Date and time picker | | file | File upload input | | tel | Telephone input | | url | URL input | | color | Color picker | | hidden | Hidden input | | custom | Any custom type you register |

Translations

With i18next

import { setupSnowForm } from '@snowpact/react-rhf-zod-form';
import i18next from 'i18next';

setupSnowForm({
  translate: i18next.t.bind(i18next),
  components: { /* ... */ },
});

With next-intl

import { setupSnowForm } from '@snowpact/react-rhf-zod-form';
import { useTranslations } from 'next-intl';

// In a client component
function SetupProvider({ children }) {
  const t = useTranslations('form');

  useEffect(() => {
    setupSnowForm({
      translate: t,
      components: { /* ... */ },
    });
  }, [t]);

  return children;
}

Schema Refinements

SnowForm fully supports Zod's refine() and superRefine() for cross-field validation:

const schema = z
  .object({
    password: z.string().min(8, 'Password must be at least 8 characters'),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords don't match",
    path: ['confirmPassword'],
  });

// Works with superRefine too
const dateSchema = z
  .object({
    startDate: z.string(),
    endDate: z.string(),
  })
  .superRefine((data, ctx) => {
    if (data.startDate > data.endDate) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'End date must be after start date',
        path: ['endDate'],
      });
    }
  });

// Use directly - refinements are fully validated on submit
<SnowForm schema={schema} onSubmit={handleSubmit} />

Overrides

Override auto-detected field types or add custom configuration:

<SnowForm
  schema={schema}
  overrides={{
    password: { type: 'password' },
    bio: { type: 'textarea' },
    website: {
      type: 'url',
      label: 'Your Website',
      placeholder: 'https://...',
      description: 'Optional personal website',
      emptyAsNull: true, // Transform empty string to null on submit
    },
  }}
/>

Available Override Options

interface FieldConfig {
  label?: string;           // Custom label
  description?: string;     // Help text below field
  placeholder?: string;     // Input placeholder
  disabled?: boolean;       // Disable the field
  type?: FieldType;         // Override detected type
  options?: FieldOption[]; // For select fields
  emptyAsNull?: boolean;    // Convert '' to null on submit
  emptyAsUndefined?: boolean; // Convert '' to undefined on submit
  emptyAsZero?: boolean;    // Convert '' to 0 for numbers
  render?: (props) => JSX;  // Custom render function
  componentProps?: object;  // Pass-through props to component
}

Custom Render Function

Use the render option for full control over a specific field:

<SnowForm
  schema={schema}
  overrides={{
    avatar: {
      label: 'Profile Picture',
      render: ({ value, onChange, error }) => (
        <div>
          <ImageUploader
            value={value}
            onChange={(url) => onChange(url)}
          />
          {error && <span className="text-red-500">{error}</span>}
        </div>
      ),
    },
    rating: {
      render: ({ value, onChange }) => (
        <StarRating
          value={value ?? 0}
          onChange={onChange}
          max={5}
        />
      ),
    },
  }}
/>

The render function receives:

  • value - Current field value
  • onChange(newValue) - Function to update the value
  • error - Validation error message (if any)

Adding New Field Types

You can register entirely new field types (like rating, rich-text, color-picker, etc.) that don't exist in the built-in types:

1. Declare the new type (for TypeScript support):

// src/types/snow-form.d.ts
declare global {
  interface SnowFormCustomTypes {
    'rating': true;
    'rich-text': true;
  }
}
export {};

2. Register the component:

setupSnowForm({
  translate: (key) => key,
  components: {
    // Built-in types
    text: MyInput,
    select: MySelect,

    // Your custom types
    'rating': ({ value, onChange, name }) => (
      <StarRating
        id={name}
        value={value ?? 0}
        onChange={onChange}
        max={5}
      />
    ),
    'rich-text': ({ value, onChange, name }) => (
      <RichTextEditor
        id={name}
        content={value}
        onChange={onChange}
      />
    ),
  },
  // ...
});

3. Use in your schema:

<SnowForm
  schema={schema}
  overrides={{
    rating: { type: 'rating', label: 'Your Rating' },
    content: { type: 'rich-text', label: 'Article Content' },
  }}
/>

Children Pattern

For full layout control, use the children render pattern:

<SnowForm schema={schema} onSubmit={handleSubmit}>
  {({ renderField, renderSubmitButton, form }) => (
    <div className="grid grid-cols-2 gap-4">
      {/* Render multiple fields at once */}
      {renderField('firstName', 'lastName')}

      {/* Or render individually for custom wrappers */}
      <div className="col-span-2">{renderField('email')}</div>

      {/* Conditional fields */}
      {form.watch('showBio') && renderField('bio')}

      <div className="col-span-2">
        {renderSubmitButton({ children: 'Create Account' })}
      </div>
    </div>
  )}
</SnowForm>

API Reference

SnowForm Props

interface SnowFormProps<TSchema, TResponse = unknown> {
  schema: TSchema;                      // Zod schema
  overrides?: Record<string, FieldConfig>; // Field customizations
  defaultValues?: Partial<z.infer<TSchema>>; // Initial values
  fetchDefaultValues?: () => Promise<...>;   // Async initial values
  onSubmit?: (values) => Promise<TResponse>; // Submit handler
  onSuccess?: (response: TResponse) => void; // Success callback
  onSubmitError?: (setErrors, error) => void; // Error handler
  debug?: boolean;                      // Enable console logging
  className?: string;                   // Form element class
  id?: string;                          // Form element id
  children?: (helpers) => ReactNode;    // Custom layout
}

Exported Functions

| Function | Description | | ------------------------------------ | ---------------------------------------------- | | setupSnowForm(options) | Initialize SnowForm (call once at app startup) | | resetSnowForm() | Reset all registries (mainly for testing) | | registerComponents(map) | Register multiple input components | | registerComponent(type, component) | Register single input component | | registerFormUI(components) | Register form UI components (label, etc.) | | registerSubmitButton(component) | Register submit button | | setTranslationFunction(fn) | Set translation function | | setOnErrorBehavior(callback) | Set error behavior | | setFormStyles(styles) | Set CSS classes for form and formItem | | normalizeDateToISO(date) | Convert date to ISO string |

Exported Types

import type {
  SnowFormProps,
  SnowFormHelpers,
  RegisteredComponentProps,
  SubmitButtonProps,
  FieldConfig,
  FieldOption,
  FieldType,
  SetupSnowFormOptions,
  TranslationFunction,
  OnErrorBehavior,
  FormStyles,
  // Form UI types
  FormUIComponents,
  FormUILabelProps,
  FormUIDescriptionProps,
  FormUIErrorMessageProps,
} from '@snowpact/react-rhf-zod-form';

License

MIT

Contributing

Contributions are welcome! Please read our contributing guidelines before submitting a PR.