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

mui-schema-form-builder

v1.7.0

Published

Schema-driven, type-safe form builder for MUI + React Hook Form + Zod

Readme

mui-schema-form-builder

Schema-driven, type-safe form builder for MUI + React Hook Form + Zod

Generate complex, production-ready forms from a plain JSON config. No boilerplate. No manual register calls. Full TypeScript inference from your Zod schema through to your onSubmit handler.


Features

  • Zero-config forms — one fields array, one schema, done
  • Type-safe submitonSubmit data is fully typed from your Zod schema
  • MUI-native — built on @mui/material v9, not bolted on
  • Password inputFIELD_TYPE.PASSWORD with a built-in show/hide toggle (no icon library needed)
  • Combo inputFIELD_TYPE.COMBO_INPUT fuses a Select dropdown with a text/number/search input into a single compound field
  • Search inputFIELD_TYPE.SEARCH is a ready-to-use search field with a magnifying-glass icon pre-wired — no startAdornment config needed
  • Filter formFilterForm fires onChange on every keystroke — no submit button, no schema required
  • Input adornmentsstartAdornment / endAdornment on TEXT and NUMBER fields for prefixes, suffixes, and icons
  • Form title — optional heading with alignment (titleAlign) and placement (titlePosition) control
  • Custom action buttons — replace Submit/Cancel/Reset with your own layout via renderActions
  • Multi-step wizardFormWizard with per-step validation, completed-step navigation, and submit-error navigation
  • Async autocomplete — debounced fetch with built-in stale-response protection
  • Conditional fields — hide/show fields based on other field values
  • Performance — fields without visibleIf never re-render on sibling changes
  • Accessible — proper <label htmlFor>, aria-required, aria-invalid, aria-describedby
  • Virtualization — optional react-window support for 50+ field forms

Installation

npm install mui-schema-form-builder

Peer dependencies (install these if you don't have them):

npm install react react-dom @mui/material @emotion/react @emotion/styled \
            react-hook-form @hookform/resolvers zod

Optional (only needed when virtualize={true}):

npm install react-window

Quick Start

import { z } from 'zod';
import { FormBuilder, FIELD_TYPE } from 'mui-schema-form-builder';
import { ThemeProvider, createTheme } from '@mui/material';

const schema = z.object({
  name: z.string().min(2, 'Name is too short'),
  email: z.string().email('Invalid email'),
  age: z.number().min(18, 'Must be 18+'),
});

const fields = [
  { name: 'name', label: 'Full Name', type: FIELD_TYPE.TEXT, required: true },
  { name: 'email', label: 'Email Address', type: FIELD_TYPE.TEXT, required: true },
  { name: 'age', label: 'Age', type: FIELD_TYPE.NUMBER, required: true },
];

export default function App() {
  return (
    <ThemeProvider theme={createTheme()}>
      <FormBuilder
        fields={fields}
        schema={schema}
        onSubmit={(data) => {
          // data.name  → string  (TypeScript inferred from schema)
          // data.email → string
          // data.age   → number
          console.log(data);
        }}
      />
    </ThemeProvider>
  );
}

Field Schema Reference

| Property | Type | Required | Description | | ---------------- | -------------------------------------- | -------- | -------------------------------------------------------------- | | name | string | ✓ | Field name — must match a key in your Zod schema | | label | string | ✓ | Display label | | type | string | ✓ | See field types below | | defaultValue | unknown | | Initial value | | placeholder | string | | Input placeholder | | required | boolean | | Shows asterisk, sets aria-required | | disabled | boolean | | Disables the field | | options | Option[] | | For SELECT, RADIO, CHECKBOX | | multiple | boolean | | Multi-select for SELECT and AUTOCOMPLETE | | grid | GridConfig | | MUI Grid size — e.g. { xs: 12, sm: 6 } | | size | 'small' \| 'medium' | | MUI component size | | fullWidth | boolean | | Full-width input (default true) | | min | number | | Min value — NUMBER only; also sets HTML min | | max | number | | Max value — NUMBER only; also sets HTML max | | step | number | | Step — NUMBER only; also sets HTML step | | rows | number | | Visible text rows — TEXTAREA only (default 4) | | startAdornment | React.ReactNode | | Prefix node inside the input (TEXT, NUMBER). E.g. "$", icon | | endAdornment | React.ReactNode | | Suffix node inside the input (TEXT, NUMBER). E.g. "kg" | | fetchOptions | (query: string) => Promise<Option[]> | | Async options for AUTOCOMPLETE | | visibleIf | (values: FieldValues) => boolean | | Hides field when returns false | | muiProps | Record<string, any> | | Extra props forwarded to the underlying MUI component | | section | string | | Groups consecutive same-section fields under a shared header | | selectOptions | Option[] | | Dropdown options — COMBO_INPUT only | | selectPosition | 'start' \| 'end' | | Which side the Select appears on — COMBO_INPUT (default 'start') | | selectPlaceholder | string | | Placeholder shown when no option is selected — COMBO_INPUT only | | inputType | 'text' \| 'number' \| 'search' | | HTML input type for the text portion — COMBO_INPUT (default 'text') | | selectWidth | number | | Width in px of the Select portion — COMBO_INPUT (default 120) |

Field Types

import { FIELD_TYPE } from 'mui-schema-form-builder';

FIELD_TYPE.TEXT;         // <input type="text">
FIELD_TYPE.TEXTAREA;     // <textarea> (multiline)
FIELD_TYPE.NUMBER;       // <input type="number">
FIELD_TYPE.DATE;         // <input type="date">
FIELD_TYPE.PASSWORD;     // Password input with show/hide toggle
FIELD_TYPE.SELECT;       // <Select> single or multi
FIELD_TYPE.AUTOCOMPLETE; // <Autocomplete> static or async
FIELD_TYPE.RADIO;        // <RadioGroup>
FIELD_TYPE.CHECKBOX;     // Boolean or checkbox group
FIELD_TYPE.ARRAY;        // Dynamic list with add/remove (useFieldArray)
FIELD_TYPE.DATE_PICKER;  // MUI DatePicker — register via createDatePickerInput
FIELD_TYPE.COMBO_INPUT;  // Fused Select + text/number/search — value shape: { select, input }
FIELD_TYPE.SEARCH;       // Text input with magnifying-glass icon pre-wired, type="search"

FilterForm

FilterForm is a reactive filter bar that fires onChange on every field change. No submit button, no Zod schema required — designed for search bars, filter sidebars, and toolbar filters.

import { FilterForm, FIELD_TYPE } from 'mui-schema-form-builder';

<FilterForm
  fields={[
    { name: 'search', label: 'Search', type: FIELD_TYPE.TEXT, placeholder: 'Search…', grid: { xs: 12, sm: 8 } },
    {
      name: 'status',
      label: 'Status',
      type: FIELD_TYPE.SELECT,
      options: [
        { label: 'All', value: '' },
        { label: 'Active', value: 'active' },
        { label: 'Inactive', value: 'inactive' },
      ],
      grid: { xs: 12, sm: 4 },
    },
  ]}
  onChange={(values) => console.log(values)}
  showReset
  defaultValues={{ search: '', status: '' }}
/>

| Prop | Type | Default | Description | | --------------- | ------------------------------- | ------- | ---------------------------------------------------- | | fields | FieldConfig[] | required | Field configuration | | onChange | (values: FieldValues) => void | required | Called on every field change with the full state | | defaultValues | FieldValues | | Initial field values | | showReset | boolean | false | Show a Reset button that restores defaultValues | | spacing | number | 2 | MUI Grid spacing between fields | | resetText | string | 'Reset' | Reset button label |


Combo Input

FIELD_TYPE.COMBO_INPUT fuses a Select dropdown with a text, number, or search input into a single compound field. The RHF value shape is { select, input }.

// Phone number — country code at start
{
  name: 'phone',
  label: 'Phone number',
  type: FIELD_TYPE.COMBO_INPUT,
  required: true,
  selectOptions: [
    { label: '+1', value: '+1' },
    { label: '+44', value: '+44' },
    { label: '+91', value: '+91' },
  ],
  selectPlaceholder: 'Code',
  selectWidth: 88,
  placeholder: '(555) 000-0000',
}

// Currency amount — selector at start, number input
{
  name: 'price',
  label: 'Price',
  type: FIELD_TYPE.COMBO_INPUT,
  selectOptions: [{ label: 'USD $', value: 'usd' }, { label: 'EUR €', value: 'eur' }],
  selectPlaceholder: 'Currency',
  selectWidth: 104,
  inputType: 'number',
  min: 0,
  placeholder: '0.00',
}

// Search with category — selector at end
{
  name: 'query',
  label: 'Search',
  type: FIELD_TYPE.COMBO_INPUT,
  selectPosition: 'end',
  selectOptions: [{ label: 'All', value: '' }, { label: 'Books', value: 'books' }],
  selectPlaceholder: 'Category',
  selectWidth: 130,
  inputType: 'search',
  placeholder: 'Search products…',
}

The Zod schema for a COMBO_INPUT field uses a nested object:

const schema = z.object({
  phone: z.object({
    select: z.string().min(1, 'Select a country code'),
    input: z.string().min(6, 'Enter a valid phone number'),
  }),
});

Note: When inputType: 'search', a magnifying-glass icon is automatically rendered as a start adornment — no extra configuration needed.


Search Input

FIELD_TYPE.SEARCH renders a text input with a magnifying-glass icon automatically pre-wired as the start adornment and type="search" set on the HTML input. No extra configuration required.

{
  name: 'search',
  label: 'Search',
  type: FIELD_TYPE.SEARCH,
  placeholder: 'Search products…',
}

Use it anywhere a FIELD_TYPE.TEXT field would go — especially inside FilterForm:

<FilterForm
  fields={[
    { name: 'search', label: 'Search', type: FIELD_TYPE.SEARCH, placeholder: 'Search…', grid: { xs: 12, sm: 8 } },
    { name: 'status', label: 'Status', type: FIELD_TYPE.SELECT, options: [...], grid: { xs: 12, sm: 4 } },
  ]}
  onChange={(values) => console.log(values)}
/>

You can still override the icon via startAdornment, or add an endAdornment (e.g. a clear button):

{ name: 'search', label: 'Search', type: FIELD_TYPE.SEARCH, endAdornment: <ClearButton /> }

Note: For a fused Select + search input (e.g. category + keyword), use FIELD_TYPE.COMBO_INPUT with inputType: 'search' instead.


Password Input

Use FIELD_TYPE.PASSWORD for a text input with a built-in show/hide toggle. The toggle uses an inline SVG icon — no @mui/icons-material dependency needed.

{
  name: 'password',
  label: 'Password',
  type: FIELD_TYPE.PASSWORD,
  required: true,
}

Input Adornments

Add a prefix or suffix decoration to TEXT and NUMBER fields via startAdornment and endAdornment. Pass any React node — a string, an icon, or an interactive element.

[
  {
    name: 'price',
    label: 'Price',
    type: FIELD_TYPE.NUMBER,
    startAdornment: '$',
    endAdornment: 'USD',
  },
  {
    name: 'username',
    label: 'Username',
    type: FIELD_TYPE.TEXT,
    startAdornment: '@',
  },
]

Note: PASSWORD fields have their own fixed end adornment (the visibility toggle). Setting endAdornment on a PASSWORD field has no effect.


Form Title

Add a heading to FormBuilder or FormWizard with the title, titleAlign, and titlePosition props.

<FormBuilder
  title="Edit Profile"
  titleAlign="left"        // 'left' | 'center' | 'right' — default: 'left'
  titlePosition="inside"   // 'inside' | 'above' — default: 'inside'
  ...
/>
  • titlePosition="inside" — the heading renders inside the Paper container above the fields.
  • titlePosition="above" — the heading renders outside the Paper, useful when you control the container styling.

Custom Action Buttons

Replace the default Submit / Cancel / Reset buttons with your own layout via renderActions.

FormBuilder

import type { FormBuilderActionsParams } from 'mui-schema-form-builder';

<FormBuilder
  onSubmit={fn}
  onCancel={cancelFn}
  onReset={resetFn}
  renderActions={({ isSubmitting, submit, cancel, reset }: FormBuilderActionsParams) => (
    <Stack direction="row" spacing={1} justifyContent="flex-end">
      {cancel && <Button onClick={cancel}>Discard</Button>}
      <Button variant="contained" onClick={submit} disabled={isSubmitting}>
        Save Changes
      </Button>
    </Stack>
  )}
/>

FormWizard

import type { FormWizardActionsParams } from 'mui-schema-form-builder';

<FormWizard
  steps={steps}
  schema={schema}
  onSubmit={fn}
  renderActions={({
    isSubmitting, isFirstStep, isLastStep, next, back, submit,
  }: FormWizardActionsParams) => (
    <Stack direction="row" spacing={2} justifyContent="space-between" width="100%">
      <Button onClick={back} disabled={isFirstStep}>← Back</Button>
      {isLastStep
        ? <Button variant="contained" onClick={submit}>Finish</Button>
        : <Button variant="contained" onClick={next}>Continue →</Button>
      }
    </Stack>
  )}
/>

Validation

Pass any Zod schema. The library uses @hookform/resolvers/zod internally:

const schema = z
  .object({
    password: z.string().min(8).regex(/[A-Z]/, 'Needs uppercase'),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    message: 'Passwords must match',
    path: ['confirm'],
  });

Control when validation runs:

<FormBuilder
  validationMode="onChange"  // 'onChange' | 'onBlur' | 'onTouched' | 'onSubmit'
  ...
/>

Conditional Fields

Only fields with visibleIf subscribe to form state changes. All other fields are isolated — typing in one field does not re-render its siblings.

const fields = [
  {
    name: 'status',
    label: 'Status',
    type: FIELD_TYPE.SELECT,
    options: [
      { label: 'Employed', value: 'employed' },
      { label: 'Student', value: 'student' },
    ],
  },
  {
    name: 'company',
    label: 'Company',
    type: FIELD_TYPE.TEXT,
    visibleIf: (values) => values['status'] === 'employed',
  },
];

Async Autocomplete

Built-in 300ms debounce and stale-response protection. If a later search resolves before an earlier one, the earlier response is discarded.

{
  name: 'country',
  label: 'Country',
  type: FIELD_TYPE.AUTOCOMPLETE,
  fetchOptions: async (query) => {
    const res = await fetch(`/api/countries?q=${query}`);
    const data = await res.json();
    return data.map((c: Country) => ({ label: c.name, value: c.code }));
  },
}

FormBuilder Props

| Prop | Type | Default | Description | | -------------------- | --------------------------------------------------- | ------------- | ---------------------------------------------------------------- | | fields | FieldConfig[] | required | Field configuration | | schema | z.ZodType | required* | Zod validation schema (*or resolver) | | resolver | Resolver | | react-hook-form resolver (alternative to schema) | | onSubmit | (data: z.infer<TSchema>) => void \| Promise<void> | required | Typed submit handler | | onCancel | () => void | | Renders Cancel button when provided | | onReset | () => void | | Renders Reset button when provided | | onChange | (values: FieldValues) => void | | Called on every field value change | | onFieldChange | (name: string, value: unknown) => void | | Called when a single field changes | | submitText | string | 'Submit' | Submit button label | | cancelText | string | 'Cancel' | Cancel button label | | resetText | string | 'Reset' | Reset button label | | title | string | | Optional form heading | | titleAlign | 'left' \| 'center' \| 'right' | 'left' | Horizontal alignment of the title | | titlePosition | 'inside' \| 'above' | 'inside' | Whether the title is inside or above the Paper container | | renderActions | (params: FormBuilderActionsParams) => ReactNode | | Replace default buttons with a custom render | | readOnly | boolean | false | Render all fields as display text | | labels | FormBuilderLabels | | Override built-in UI strings | | spacing | number | 2 | MUI Grid spacing between fields | | virtualize | boolean | false | Enable react-window for large forms | | validationMode | ValidationMode | 'onTouched' | When validation triggers | | sx | SxProps | | MUI sx prop for the outer Paper |


TypeScript Tips

The generic propagates from schema → onSubmit automatically:

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

<FormBuilder
  schema={schema}
  fields={fields}
  onSubmit={(data) => {
    // data.name → string ✓
    // data.age  → number ✓
  }}
/>;

Memoize your fields array to prevent unnecessary recomputation of default values:

const fields = useMemo<FieldConfig[]>(
  () => [{ name: 'name', label: 'Name', type: FIELD_TYPE.TEXT }],
  [],
);

Accessibility

  • Every input has a proper <label htmlFor> association — clicking the label focuses the input
  • Required asterisk is aria-hidden (visual cue only)
  • Inputs have aria-required, aria-invalid, aria-describedby linked to error messages
  • Error messages have role="alert" for screen reader announcement
  • Radio groups and checkbox groups use <fieldset> + <legend> (WCAG 1.3.1)

License

MIT © Arjun Prakash