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

@aidanbell/schema-form-ui

v0.2.0

Published

Styled SchemaForm for @aidanbell/schema-form

Readme

@aidanbell/schema-form-ui

Experimental 0.1.x — API may change before 1.0.

Styled, accessible React forms driven by the same JSON schema as @aidanbell/schema-form. Uses Tailwind utility classes and Base UI for the checkbox primitive.

This package does not ship a CSS file. Your app must run Tailwind and scan this package’s built files so the utility classes in dist are included.

Install

pnpm add @aidanbell/schema-form-ui @aidanbell/schema-form valibot react react-dom react-hook-form @hookform/resolvers

Peers: react / react-dom ≥18.2, @aidanbell/schema-form ≥0.1.0, valibot ^1.4.2, react-hook-form ^7.50, @hookform/resolvers ≥5.0.1.

Runtime dependencies (installed with the package): clsx, tailwind-merge, @base-ui/react.

Tailwind setup (required)

Without this, controls look unstyled because Tailwind never sees the class strings in node_modules.

Tailwind v4

@import "tailwindcss";
@source "../node_modules/@aidanbell/schema-form-ui/dist/**/*.{js,mjs}";

Tailwind v3

// tailwind.config.js
content: [
  "./src/**/*.{js,ts,jsx,tsx}",
  "./node_modules/@aidanbell/schema-form-ui/dist/**/*.{js,mjs}",
],

Adjust the relative path to match where your CSS / config file lives.

Quickstart

import { SchemaForm } from "@aidanbell/schema-form-ui";

const schema = {
  schemaVersion: 1 as const,
  title: "Sign up",
  description: "Create an account",
  fields: [
    { name: "email", type: "email" as const, label: "Email", required: true },
    {
      name: "role",
      type: "select" as const,
      label: "Role",
      options: [
        { label: "Admin", value: "admin" },
        { label: "Viewer", value: "viewer" },
      ],
    },
  ],
};

export function SignupForm() {
  return (
    <SchemaForm
      config={{ schema }}
      onSubmit={(values) => {
        console.log(values);
      }}
      onError={(error) => {
        console.error(error);
      }}
    />
  );
}

Invalid schemas render an error UI and call onError with the parse result — the app does not crash. If onSubmit throws or rejects, onError receives that error as well.

Theming with classNames

Pass Tailwind classes via config.classNames (form-wide) and/or config.fields[name].classNames (per field). Later utilities win when they conflict (cn / tailwind-merge).

<SchemaForm
  config={{
    schema,
    classNames: {
      form: "max-w-md",
      label: "font-semibold",
      submitButton: "bg-emerald-600 hover:bg-emerald-700",
    },
    fields: {
      email: { classNames: { control: "border-emerald-400" } },
      role: { disabled: true },
    },
    submitLabel: "Create account",
    showReset: false,
  }}
  onSubmit={async (values) => {
    /* ... */
  }}
/>

Field overrides: hidden, disabled, component, and nested classNames keys (field, label, description, control, error, …).

Customizing rendering

Three layers, pick the smallest one that fits. Each layer can coexist with the others; more specific wins.

1. Replace a control type everywhere — config.components

Swap the input for a given FieldType across the form. Label, description, and error chrome stay put.

import { SchemaForm, type FieldControlProps } from "@aidanbell/schema-form-ui";

function FancySelect({
  field,
  form,
  id,
  disabled,
  className,
  "aria-invalid": ariaInvalid,
  "aria-describedby": ariaDescribedBy,
}: FieldControlProps) {
  return (
    <select
      id={id}
      disabled={disabled}
      className={className}
      aria-invalid={ariaInvalid}
      aria-describedby={ariaDescribedBy}
      {...form.register(field.name)}
    >
      {(field.options ?? []).map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}

<SchemaForm
  config={{ schema, components: { select: FancySelect } }}
  onSubmit={...}
/>

2. Replace one field's control — config.fields[name].component

Same chrome, one field. Wins over a matching components entry.

<SchemaForm
  config={{
    schema,
    components: { string: FancyInput },
    fields: { nickname: { component: NicknameControl } },
  }}
  onSubmit={...}
/>

3. Own the whole row — renderField

A render prop on SchemaForm (not config). Receives the same FieldControlProps as custom controls, plus defaultRender to fall back per field.

<SchemaForm
  config={{ schema }}
  renderField={(props, defaultRender) =>
    props.field.name === "rating" ? (
      <StarRatingRow {...props} />
    ) : (
      defaultRender(props)
    )
  }
  onSubmit={...}
/>

defaultRender(props) is equivalent to not using renderField at all. You can tweak props before delegating:

renderField={(props, defaultRender) => defaultRender({ ...props, disabled: true })}

hidden still removes a field before renderField is consulted.

Custom control contract

Custom controls receive FieldControlProps: field, form, id, disabled, error, className, aria-invalid, and aria-describedby. You must wire the value into React Hook Form yourself — form.register(field.name) for native inputs, or Controller when the widget is uncontrolled. The built-in implementations in controls.tsx are the reference.

If renderField returns custom markup without calling defaultRender, you own the label, error display, and accessibility wiring. aria-describedby points at {field.name}-error and {field.name}-description; those ids only exist if you render elements with them.

API

| Export | Role | |--------|------| | SchemaForm | Parse schema → render fields + submit/reset | | SchemaField | Single field bound to a React Hook Form instance | | Built-in controls | StringControl, NumberControl, BooleanControl, TextareaControl, SelectControl, RadioControl | | cn / mergeClassNames | Tailwind-friendly class helpers | | UI primitives | Input, Textarea, Select, Checkbox, Label, Button |

Types: SchemaFormProps, SchemaFormConfig, SchemaFormClassNames, FieldControlProps.

Full field contract: SCHEMA.md in the monorepo.

Empty-value semantics

Same as core:

  • Optional text-like fields submit '' when empty.
  • Optional number fields submit undefined (never 0).
  • boolean defaults to false; required boolean must be true.

License

MIT