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

react-simple-schema-form

v0.1.3

Published

Generate React forms from JSON Schema: $ref, allOf/oneOf/anyOf, if/then/else, dependencies, custom widgets, zero dependencies.

Readme

react-simple-schema-form

Generate React forms from JSON Schema (draft-07 subset). Zero runtime dependencies beyond React.

Live demo → — pick an example, edit the schema and uiSchema, watch the form regenerate.

npm install react-simple-schema-form
import { SchemaForm } from 'react-simple-schema-form';
import 'react-simple-schema-form/styles.css'; // optional default styles
import schema from './schema.json';

<SchemaForm
  schema={schema}
  uiSchema={{ bio: { widget: 'textarea' } }}
  onSubmit={(data) => console.log(data)}
/>

Scripts

| Command | What it does | | ------------------- | --------------------------------------------- | | npm run dev | Vite playground at demo/ — pick an example from examples/, edit the schema and uiSchema live | | npm test | Vitest unit + rendering tests | | npm run typecheck | tsc --noEmit | | npm run build | ESM + CJS + .d.ts + styles.css into dist/ | | npm run build:demo| Static demo into docs/ (served by GitHub Pages) |

Supported schema keywords

| Type | Keywords | Default widget | | ----------------- | ---------------------------------------------------------------------------- | -------------- | | string | format (email, uri/url, date, date-time, time, password, color), minLength, maxLength, pattern, enum | text / by format / select | | number/integer| minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, enum | number / select | | boolean | | checkbox | | object | properties, required | fieldset (nested) | | array | items, minItems, maxItems, uniqueItems | add/remove/reorder list; checkboxes when items.enum | | any | title, description, default, const, readOnly, enumNames | |

Composition, references and conditionals

| Keyword | Behaviour | Example | | ------- | --------- | ------- | | $ref | Local JSON pointers (#/definitions/x, #/$defs/x, #). Sibling keywords override the target. Circular chains throw; self-referencing properties stop default generation at the cycle. | examples/ref-definitions.json | | allOf | Parts are deep-merged: properties recursively, required unioned, later parts override scalars. | examples/all-of.json | | oneOf / anyOf | Rendered as a branch selector plus the chosen branch. Keywords next to the combinator (shared properties, required) apply to every branch. If a branch has a const discriminator, changing that field switches the branch automatically. A combinator whose branches are all const becomes a labelled select. Branches that carry only constraints (e.g. anyOf: [{ required: ["a"] }, { required: ["b"] }]) are a rule, not a choice: no selector, and one error on the node — "Provide at least one of: A, B". | examples/one-of.json | | if / then / else | Evaluated against the current data on every change; then/else are merged in. Nest inside allOf for several independent conditions. | examples/if-then-else.json | | dependencies | Property form ({"a": ["b"]}) adds required; schema form merges a sub-schema. dependentRequired / dependentSchemas (2019-09) work the same way. Only active when the trigger property is non-empty. | examples/dependencies.json |

Recipe — an optional section that is validated only once enabled. Put the toggle inside the object as a boolean and hang the requirements off it with if/then. An optional object that nobody has touched is treated as absent (no errors); once the toggle is on the object is present and the then requirements apply. See schedule in schema.json and the demo's SchedulerWidget, which renders the switch, offers an explicit Run once / Repeat choice that clears the other mode's fields, and resets the object on disable so half-entered values can never block submission:

"schedule": {
  "type": "object",
  "ui:widget": "scheduler",
  "properties": {
    "enabled":    { "type": "boolean", "title": "Enable schedule", "default": false },
    "runAt":      { "type": "integer", "title": "Run at" },
    "intervalMs": { "type": "integer", "title": "Repeat every", "minimum": 1000 }
  },
  "if":   { "properties": { "enabled": { "const": true } }, "required": ["enabled"] },
  "then": { "oneOf": [{ "required": ["runAt"] }, { "required": ["intervalMs"] }] }
}

The "required": ["enabled"] inside if matters: without it an object with no enabled key at all would satisfy the condition. The oneOf of required is a rule, not a UI choice: both fields render, and setting both yields "Choose only one of: Run at, Repeat every". Rules that compare two values ("the window must end after it starts") go in the validate prop — see the demo's rules.ts.

Validation follows the same resolution: for oneOf/anyOf the errors shown are those of the branch the data belongs to (matched on everything except required), so users get field-level messages rather than a bare "no match".

Not supported: remote $refs, not, additionalProperties as a schema, patternProperties, contains.

Choosing widgets

Precedence, highest first:

  1. uiSchema prop — exact path, then globs from most to least specific
  2. parent's nested uiSchema keyword (see below) — folded onto the child during resolution
  3. inline ui:widget on the schema node
  4. resolveWidget prop — a rule function
  5. built-in default from type / format / enum

The app's uiSchema always beats the schema, so a field can be restyled without editing a schema that may be shared or served by a backend.

Globs in uiSchema keys cover arrays and reused $refs without listing every path:

uiSchema={{
  'tags.*':        { widget: 'tag' },       // every item of tags
  '*.street':      { placeholder: '…' },    // street in any top-level object
  '**.postalCode': { widget: 'postal' },    // postalCode at any depth
}}

* matches one segment, ** any number. When several keys match, the most specific wins per option (exact > more literal segments > * > **); options from different keys merge, so a glob can add help while an exact key sets widget.

resolveWidget selects by rule and sees the fully resolved schema:

const resolveWidget: ResolveWidget = ({ schema, path, defaultWidget }) => {
  if (schema.format === 'epoch') return 'epoch';         // a registered name
  if (schema['x-widget']) return schema['x-widget'];     // your own keyword
  if (path.endsWith('.notes')) return NotesWidget;       // or a component directly
  return undefined;                                      // fall through to defaults
};
<SchemaForm schema={schema} widgets={{ epoch: EpochWidget }} resolveWidget={resolveWidget} />

defaultWidget is what the library would pick on its own — undefined for objects and non-enum arrays, which otherwise render structurally.

Nested uiSchema keyword. An object node can carry hints for its children keyed by property name (items for arrays), nesting as deep as needed. Entries accept ui:* or plain option names. They are folded onto the children during resolution and override the children's own inline ui:*, so a $ref site can restyle the definition it points at:

"schedule": {
  "type": "object",
  "ui:widget": "scheduler",                  // widget for the object itself
  "uiSchema": {
    "runAt":   { "ui:widget": "epoch" },     // hints for its children
    "maxRuns": { "widget": "counter" }       // plain names work too
  },
  "properties": { "runAt": { "type": "integer" }, "maxRuns": { "type": "integer" } }
}

This is the shape of schedule in schema.json.

Unregistered names don't break the form: the library warns once in the console and renders the built-in default for that field (structurally, for objects and arrays).

Widgets on object/array nodes. Selecting a widget for an object or array replaces the default fieldset/list. The widget gets the whole value, and can render children itself with the exported <Field> (see InlineAddressWidget). props.errors contains the errors of the node and its descendants, regardless of touched state, so such a widget can summarise what's wrong inside; props.invalid stays the "should show an error now" flag.

UI hints: uiSchema prop and inline ui:* keywords

The same options can live in either place; the uiSchema prop wins when both are set.

// inline, in the schema itself
{ "type": "integer", "title": "Starts at", "ui:widget": "epoch", "ui:help": "Unix seconds" }
// external, keyed by dot path
<SchemaForm schema={schema} uiSchema={{ startsAt: { widget: 'epoch', help: 'Unix seconds' } }} />

Options: widget, placeholder, help, disabled, props (forwarded to the widget element; props objects from both sources merge). See examples/ui-widgets.json and examples/widget-selection.json.

<SchemaForm> props

| Prop | Type | Notes | | -------------- | ----------------------------------------- | ----- | | schema | JSONSchema | Required. | | uiSchema | Record<path, UiFieldOptions> | Keyed by dot path (address.city, tags.0). Options: widget, placeholder, help, disabled, props. | | value | T | Controlled data; pair with onChange. | | defaultValue | Partial<T> | Uncontrolled initial data, merged with schema defaults. | | onChange | (data, errors) => void | Fires on every edit with the fresh validation result. | | onSubmit | (data) => void | Only called when there are no validation errors. | | onError | (errors) => void | Called on submit when invalid; the first invalid field is focused. | | validate | (data, schemaErrors) => FieldError[] | Cross-field rules the schema can't express (e.g. "end after start"); results show under their path and block submit. | | widgets | Record<string, Widget> | Add or replace widgets by name. | | resolveWidget| (ctx) => name \| Widget \| undefined | Rule-based widget selection; see Choosing widgets. | | disabled / readOnly | boolean | | | id | string | Id prefix; set it when rendering more than one form per page. | | submitLabel | string | Default "Submit". | | children | ReactNode | Replace the submit button; pass null to omit it. |

Field errors are shown once a field is blurred, or for every field after a submit attempt.

Custom widgets

A widget is a component receiving WidgetProps<T>. Register it under a name with the widgets prop, then reference that name from ui:widget or uiSchema. The demo's EpochWidget stores a Unix timestamp (type: integer) but renders a datetime-local picker:

import { SchemaForm, type Widget } from 'react-simple-schema-form';

const EpochWidget: Widget<number | undefined> = ({ id, value, onChange, onBlur, disabled }) => (
  <input
    type="datetime-local"
    id={id}
    disabled={disabled}
    value={value === undefined ? '' : new Date(value * 1000).toISOString().slice(0, 16)}
    onBlur={onBlur}
    onChange={(e) => {
      const ms = new Date(e.target.value).getTime();
      onChange(Number.isNaN(ms) ? undefined : Math.floor(ms / 1000));
    }}
  />
);

<SchemaForm
  schema={{ type: 'object', properties: { startsAt: { type: 'integer', 'ui:widget': 'epoch' } } }}
  widgets={{ epoch: EpochWidget }}
/>

Another example, overriding the number widget with a range slider:

import type { Widget } from 'react-simple-schema-form';

const Slider: Widget<number | undefined> = ({ id, value, onChange, onBlur, schema, disabled }) => (
  <input
    type="range"
    id={id}
    min={schema.minimum}
    max={schema.maximum}
    value={value ?? schema.minimum ?? 0}
    disabled={disabled}
    onBlur={onBlur}
    onChange={(e) => onChange(Number(e.target.value))}
  />
);

<SchemaForm schema={schema} widgets={{ slider: Slider }} uiSchema={{ age: { widget: 'slider' } }} />

Built-in widget names: text, email, password, url, date, datetime, time, color, textarea, number, checkbox, select, radio, checkboxes, hidden. Overriding one of these in widgets changes the default for every field that resolves to it.

Standalone helpers

import { validate, getDefaultFormData, resolveSchema, resolveOptions } from 'react-simple-schema-form';

validate(schema, data);                              // FieldError[] — { path, keyword, message }
getDefaultFormData(schema);                          // initial data from `default` / `minItems` / `const`
resolveSchema(node, data, resolveOptions(root));     // flatten $ref/allOf/if/dependencies for this data

For AI assistants and agents

  • llms.txt — ssunils.github.io/react-simple-schema-form/llms.txt (index) and llms-full.txt (README + skill + every example in one file). Paste either into a chat, or point a docs MCP at them.
  • Agent skill — skills/react-simple-schema-form/SKILL.md ships inside the npm package. Claude Code, Cursor and other Agent-Skills-compatible tools can load it from node_modules/react-simple-schema-form/skills/; or copy it into your project's .claude/skills/ (or equivalent) so the agent knows the API, widget precedence and recipes without reading source.
  • Context7 — the repo carries a context7.json so resolve-library-id react-simple-schema-form returns focused docs.
  • Every export has JSDoc, so the shipped .d.ts explains itself in editors.

Styling

All elements carry sf-* class names (sf-form, sf-field, sf-field--error, sf-label, sf-input, sf-select, sf-error, sf-object, sf-array, sf-btn, …). The shipped styles.css is a small, theme-agnostic default driven by CSS variables (--sf-border, --sf-border-focus, --sf-error, --sf-radius); skip the import to bring your own.