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

schema-components

v3.7.1

Published

React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents

Readme

schema-components

npm version License: MIT GitHub Workflow Status

React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents.

Install

npm install schema-components

Peer dependencies: zod@^4.0.0, react@^18.0.0 || ^19.0.0. preact@>=10.0.0 is also accepted as an optional peer for the Preact entry point — see Preact support.

Zod version requirement

schema-components requires Zod 4. If you are on Zod 3, see the Zod 4 migration guide. Zod 3 schemas are detected structurally — any object exposing _def without the Zod 4 _zod marker is classified as Zod 3, with or without the historical _def.typeName field. (Some third-party Zod-3-style libraries omit typeName; the detector keys on the presence of _def alone.) A descriptive SchemaNormalisationError is raised pointing at the Zod 4 migration guide.

Schemas from other libraries that conform to the Standard Schema spec (valibot, arktype, ...) are also detected and rejected. When the input advertises a ~standard.vendor field, the error message includes the vendor name so consumers know which library produced the input.

Preact support

Preact is supported as an optional peer dependency via preact/compat aliasing. The Preact entry point (schema-components/preact/*) re-exports every public symbol from the React entry point (SchemaComponent, SchemaProvider, SchemaField, SchemaView, SchemaErrorBoundary, registerWidget, headlessResolver). The renderer tree is identical — preact/compat translates React-style onChange to onInput at render time, matching the "fires on every keystroke" semantics the controlled inputs rely on.

Import from schema-components/preact/* instead of schema-components/react/*, then configure your bundler to alias react and react-dom to preact/compat.

import { SchemaComponent } from "schema-components/preact/SchemaComponent";

Vite

// vite.config.ts
import { defineConfig } from "vite";

export default defineConfig({
    resolve: {
        alias: {
            "react-dom/test-utils": "preact/test-utils",
            "react-dom/client": "preact/compat/client",
            "react-dom/server": "preact/compat/server",
            "react-dom": "preact/compat",
            "react/jsx-runtime": "preact/jsx-runtime",
            "react/jsx-dev-runtime": "preact/jsx-dev-runtime",
            react: "preact/compat",
        },
    },
});

The order matters: longer, path-specific entries (react-dom/client, react/jsx-runtime) must come before the bare entries (react, react-dom) so they get a chance to claim the request first.

Next.js

// next.config.ts
import type { NextConfig } from "next";

const config: NextConfig = {
    webpack(webpackConfig) {
        webpackConfig.resolve.alias = {
            ...webpackConfig.resolve.alias,
            "react-dom/test-utils": "preact/test-utils",
            "react-dom/client": "preact/compat/client",
            "react-dom/server": "preact/compat/server",
            "react-dom": "preact/compat",
            "react/jsx-runtime": "preact/jsx-runtime",
            "react/jsx-dev-runtime": "preact/jsx-dev-runtime",
            react: "preact/compat",
        };
        return webpackConfig;
    },
};

export default config;

Node (without a bundler)

Use the package manager's overrides field to swap the resolved package at install time. For npm:

// package.json
{
    "overrides": {
        "react": "npm:@preact/compat@*",
        "react-dom": "npm:@preact/compat@*"
    }
}

For pnpm:

// package.json
{
    "pnpm": {
        "overrides": {
            "react": "npm:@preact/compat@*",
            "react-dom": "npm:@preact/compat@*"
        }
    }
}

For yarn, use the resolutions field with the same value.

Known limitations

  1. React Server Components is React-only. <SchemaView> runs as a client component under Preact, losing the zero-client-JS deployment story documented in Server Components. The component still renders correctly — it just executes in the browser rather than at request time on the server.
  2. onChange semantics requires preact/compat. Raw preact (without compat) fires onChange on commit / blur, matching the DOM standard. preact/compat aliases onChange to onInput so the controlled inputs fire on every keystroke, matching React's behaviour. Importing from schema-components/preact/* without aliasing react to preact/compat will leave events misrouted.
  3. HTML serialization differs slightly. Preact emits HTML5 boolean-attribute shorthand (value for an empty string) where React emits value="". The semantics are identical; only string snapshots that match the exact React form will need adjusting under Preact.
  4. Vnode prop shape differs. Preact-compat rewrites React-style event prop names (onChange, onBlur) to lowercase DOM event names (oninput, onfocusout) on the vnode prop bag during rendering. Tests that inspect a React element's .props directly (rather than the rendered DOM) need to expect the lowercase form when running under Preact.

The test suite is parametrised with a unit-preact Vitest project that runs the same files under preact/compat aliasing. Run it via pnpm test:preact from the repo root, or directly with pnpm exec vitest run --project=unit-preact from packages/core. A small set of tests (the ones bound to the three known limitations above) fail intentionally; the remaining ~99% pass under both runtimes and establish the cross-runtime regression boundary.

Vue support

Vue 3.5+ is supported as an optional peer dependency. The Vue adapter ships as source under schema-components/vue/* — the .vue Single File Components are not pre-compiled into the published tarball. Consumers need a Vite or webpack toolchain with @vitejs/plugin-vue (or equivalent) to compile the SFCs at their own build step.

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
    plugins: [vue()],
});
<script setup lang="ts">
import { ref } from "vue";
import SchemaComponent from "schema-components/vue/SchemaComponent.vue";
import { z } from "zod";

const userSchema = z.object({
    name: z.string(),
    email: z.email(),
});

const user = ref({ name: "Ada", email: "[email protected]" });
</script>

<template>
    <SchemaComponent :schema="userSchema" v-model="user" />
</template>

The ./vue/* export subpath resolves to the source tree (src/vue/*.ts) and ./vue/*.vue resolves to the .vue SFCs under src/vue/. The same pattern is used by the Svelte adapter — both rely on the consumer's bundler to handle the framework-specific compilation step.

SchemaComponent

The single entry point. Accepts Zod schemas, JSON Schema objects, or OpenAPI documents:

import { SchemaComponent } from "schema-components/react/SchemaComponent";

// Zod schema
<SchemaComponent schema={z.object({ name: z.string() })} value={data} />

// JSON Schema object
<SchemaComponent schema={{ type: "object", properties: { name: { type: "string" } } }} value={data} />

// OpenAPI document + schemaRef
<SchemaComponent schema={openApiSpec} schemaRef="#/components/schemas/User" value={data} />

Props

| Prop | Type | Description | |---|---|---| | schema | ZodType \| JSONObject \| OpenAPIDocument | The schema to render | | value | unknown | Current value for the schema | | onChange | (value: unknown) => void | Callback when value changes | | readOnly | boolean | Force read-only presentation | | writeOnly | boolean | Force write-only (blank inputs) | | schemaRef | string | JSON Pointer into OpenAPI document | | fields | InferFields<T> | Type-safe per-field overrides | | widgets | WidgetMap | Instance-scoped widget overrides | | validate | boolean | Enable Zod validation on change | | onValidationError | (error: unknown) => void | Callback for validation errors | | onError | (error: SchemaError) => ReactNode \| void | Per-component error handler | | resolver | ComponentResolver | Theme adapter override | | meta | SchemaMeta | Schema-level metadata override |

Component editability

Fields render in one of three states, controlled by readOnly and writeOnly from three sources:

| State | Rendering | |---|---| | Presentation | Read-only display. Formatted text, links, badges. No inputs. | | Input | Empty field. Blank inputs, "Select…" dropdowns, unchecked toggles. | | Editable | Pre-populated input the user can change. |

Three sources, priority order

  1. Schema property (.meta({ readOnly: true })) — always wins
  2. Component props (readOnly / writeOnly on <SchemaComponent>) — rendering context
  3. Schema root (.meta({ readOnly: true }) on root schema) — fallback default
  4. Neither → Editable

Overriding with readOnly: false

A field override can explicitly opt out of a higher-level readOnly:

<SchemaComponent
  schema={userSchema}
  value={user}
  readOnly                         // everything presentation
  fields={{
    address: {
      readOnly: false,            // address subtree: editable
      city: { readOnly: true },   // city: still presentation
    },
  }}
/>

Type-safe field overrides

The fields prop type is inferred from the schema:

// Zod — full autocomplete
<SchemaComponent
  schema={userSchema}
  fields={{
    name: { readOnly: true },            // ✓ type-safe
    address: {
      city: { description: "City" },     // ✓ nested, type-safe
    },
    // nme: { readOnly: true },          // ✗ TypeScript error: unknown key
  }}
/>

// JSON Schema as const — full autocomplete
const jsonSchema = {
  type: "object" as const,
  properties: {
    name: { type: "string" as const },
    email: { type: "string" as const, format: "email" },
  },
  required: ["name"],
} as const;

<SchemaComponent
  schema={jsonSchema}
  fields={{
    name: { readOnly: true },            // ✓ inferred from as const
    // nme: { readOnly: true },          // ✗ TypeScript error
  }}
/>

// OpenAPI as const + schemaRef — full autocomplete
const spec = {
  openapi: "3.1.0",
  components: {
    schemas: {
      User: {
        type: "object" as const,
        properties: {
          id: { type: "string" as const },
          name: { type: "string" as const },
        },
        required: ["id", "name"],
      },
    },
  },
} as const;

<SchemaComponent
  schema={spec}
  schemaRef="#/components/schemas/User"
  fields={{
    id: { readOnly: true },              // ✓ inferred through schemaRef
  }}
/>

FieldOverride

Each field override accepts:

| Property | Type | Description | |---|---|---| | readOnly | boolean | Override editability for this field | | writeOnly | boolean | Override write-only state | | visible | boolean | Hide the field entirely when false | | order | number | Sort order within parent object | | onValidationError | (error: unknown) => void | Per-field validation callback | | description | string | Override label / description | | default | unknown | Override default value | | component | string | Widget name for custom rendering |

Plus any standard JSON Schema meta properties (title, format, pattern, etc.).

Individual fields

import { SchemaField } from "schema-components/react/SchemaComponent";

// Type-safe path — only valid dot-paths accepted
<SchemaField
  schema={userSchema}
  path="address.city"      // ✓ type-safe
  // path="address.cty"   // ✗ TypeScript error
  value={user}
  onChange={setUser}
/>

When the schema is a Zod schema or typed as const, only valid dot-paths like "address.city" are accepted. Invalid paths trigger TypeScript errors. Runtime schemas accept any string.

Spec support

The walker reads canonical Draft 2020-12 JSON Schema. Older drafts and OpenAPI documents are normalised to that form transparently.

| Spec | Detection | Normalisation | Notes | |---|---|---|---| | JSON Schema Draft 04 | http://json-schema.org/draft-04/schema# | exclusiveMinimum/Maximum boolean → number; id left in place | | | JSON Schema Draft 06 | http://json-schema.org/draft-06/schema# | Pass-through (canonical for const, examples[], $id, propertyNames, contains) | | | JSON Schema Draft 07 | http://json-schema.org/draft-07/schema# | Pass-through (adds if/then/else, contentEncoding, contentMediaType) | | | JSON Schema Draft 2019-09 | https://json-schema.org/draft/2019-09/schema | $recursiveRef$ref, $recursiveAnchor$anchor | Adds unevaluatedProperties/Items, dependentSchemas/Required | | JSON Schema Draft 2020-12 | https://json-schema.org/draft/2020-12/schema (default) | $dynamicRef$ref, $dynamicAnchor$anchor; tuple form via prefixItems | | | OpenAPI 2.0 (Swagger) | swagger: "2.0" | Full document restructure → OpenAPI 3.1; definitionscomponents/schemas; body params → requestBody; formDatamultipart/form-data; collectionFormatstyle/explode | | | OpenAPI 3.0.x | openapi: "3.0.x" | nullableanyOf [T, null]; discriminator mapping → injected const; exampleexamples[] | Callbacks, links, security schemes preserved | | OpenAPI 3.1.x | openapi: "3.1.x" | Direct (already Draft 2020-12) | Webhooks, components/pathItems, JSON Schema type arrays, examples[] |

Documented type-level fallbacks

A few JSON Schema keywords can't be expressed in TypeScript's type system. They are handled at runtime by the walker, but FromJSONSchema<S> falls back as follows (each pinned by tests/type-inference-advanced.test.ts):

| Keyword | Type-level result | Why | |---|---|---| | not | unknown | TypeScript has no type negation | | if / then / else | Base schema (conditions ignored) | Requires value-dependent conditional evaluation | | propertyNames | Ignored | Cannot constrain object key shape | | dependentSchemas / dependentRequired | Ignored | Cross-field runtime conditionals | | unevaluatedProperties / unevaluatedItems | Ignored | Requires whole-tree evaluation context | | contains / minContains / maxContains | Element type unchanged | Constraint metadata only | | $recursiveRef | unknown | Recursive types not expressible |

Runtime rendering and validation handle each of these correctly; only the static type widens.

OpenAPI components

Render API operations with type-safe field overrides:

import {
  ApiOperation,
  ApiParameters,
  ApiRequestBody,
  ApiResponse,
} from "schema-components/openapi/components";

const petStore = {
  openapi: "3.1.0",
  paths: {
    "/pets": {
      post: {
        requestBody: {
          content: {
            "application/json": {
              schema: {
                type: "object" as const,
                properties: {
                  name: { type: "string" as const },
                  tag: { type: "string" as const },
                },
                required: ["name"],
              },
            },
          },
        },
        responses: { "201": { description: "Created" } },
      },
    },
  },
} as const;

// Full operation — parameters, request body, responses
<ApiOperation schema={petStore} path="/pets" method="post" />

// Just the request body with type-safe fields
<ApiRequestBody
  schema={petStore}
  path="/pets"
  method="post"
  fields={{
    name: { description: "Pet name" },    // ✓ inferred from as const
  }}
/>

// Just parameters with type-safe overrides
<ApiParameters
  schema={petStore}
  path="/pets"
  method="get"
  overrides={{
    limit: { description: "Max results" },
  }}
/>

// Response schema
<ApiResponse schema={petStore} path="/pets" method="get" status="200" />

Theme adapters

Headless by default (plain HTML). Wrap with a theme adapter for styled components:

shadcn/ui

import { SchemaProvider } from "schema-components/react/SchemaComponent";
import { shadcnResolver } from "schema-components/themes/shadcn";

<SchemaProvider resolver={shadcnResolver}>
  <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
</SchemaProvider>

MUI

import { SchemaProvider } from "schema-components/react/SchemaComponent";
import { createMuiResolver } from "schema-components/themes/mui";
import TextField from "@mui/material/TextField";
import Checkbox from "@mui/material/Checkbox";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel";

const muiResolver = createMuiResolver({
  TextField, Checkbox, Typography, Box, MenuItem, FormControlLabel,
});

<SchemaProvider resolver={muiResolver}>
  <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
</SchemaProvider>

Mantine

import { SchemaProvider } from "schema-components/react/SchemaComponent";
import { createMantineResolver } from "schema-components/themes/mantine";
import {
  TextInput, NumberInput, Switch, Select, Fieldset, Text,
} from "@mantine/core";

const mantineResolver = createMantineResolver({
  TextInput, NumberInput, Switch, Select, Fieldset, Text,
});

<SchemaProvider resolver={mantineResolver}>
  <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
</SchemaProvider>

Radix Themes

import { SchemaProvider } from "schema-components/react/SchemaComponent";
import { createRadixResolver } from "schema-components/themes/radix";
import { Box, Checkbox, Flex, Select, Text, TextField } from "@radix-ui/themes";

const radixResolver = createRadixResolver({
  Box,
  Checkbox,
  Flex,
  SelectRoot: Select.Root,
  SelectTrigger: Select.Trigger,
  SelectContent: Select.Content,
  SelectItem: Select.Item,
  Text,
  TextField: TextField.Root,
});

<SchemaProvider resolver={radixResolver}>
  <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
</SchemaProvider>

Custom adapter

import type { RenderProps, ComponentResolver } from "schema-components/core/renderer";

const myResolver: ComponentResolver = {
  string: (props: RenderProps) => {
    if (props.readOnly) return <span>{props.value}</span>;
    return <input value={props.value} onChange={(e) => props.onChange(e.target.value)} />;
  },
  object: (props: RenderProps) => {
    return (
      <div>
        {props.fields && Object.entries(props.fields).map(([key, field]) => (
          <div key={key}>
            <label>{field.meta.description}</label>
            {props.renderChild(field, (props.value as Record<string, unknown>)?.[key], (v) => {
              props.onChange({ ...(props.value as object), [key]: v });
            })}
          </div>
        ))}
      </div>
    );
  },
};

Every render function receives props.renderChild for recursive rendering — no need to know about the resolver or rendering context.

Custom widgets

Widgets let you override rendering for specific fields using .meta({ component: name }). Three scopes are available, checked in order:

  1. Instancewidgets prop on <SchemaComponent>
  2. Contextwidgets prop on <SchemaProvider>
  3. GlobalregisterWidget() for app-wide defaults

Global registration

import { registerWidget } from "schema-components/react/SchemaComponent";

registerWidget("richtext", ({ value, onChange }) => (
  <RichTextEditor value={value} onChange={onChange} />
));

const schema = z.object({
  bio: z.string().meta({ component: "richtext" }),
});

Context-scoped widgets

import { SchemaProvider } from "schema-components/react/SchemaComponent";
import type { WidgetMap } from "schema-components/react/SchemaComponent";

const adminWidgets: WidgetMap = new Map([
  ["richtext", ({ value, onChange }) => <RichTextEditor value={value} onChange={onChange} />],
  ["avatar", ({ value, onChange }) => <AvatarUploader value={value} onChange={onChange} />],
]);

<SchemaProvider resolver={shadcnResolver} widgets={adminWidgets}>
  <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
  <SchemaComponent schema={profileSchema} value={profile} onChange={setProfile} />
</SchemaProvider>

Instance-scoped widgets

const formWidgets: WidgetMap = new Map([
  ["richtext", ({ value, onChange }) => <SimpleTextarea value={value} onChange={onChange} />],
]);

<SchemaComponent schema={formSchema} value={form} widgets={formWidgets} />

Resolution order

.meta({ component }) hint → instance widgets → context widgets → global registerWidget() → theme adapter → headless default

WidgetMap type

import type { WidgetMap } from "schema-components/react/SchemaComponent";

// ReadonlyMap<string, (props: RenderProps) => unknown>
const widgets: WidgetMap = new Map([
  ["name", (props) => <MyInput {...props} />],
]);

Server Components: <SchemaView> accepts a widgets prop directly (no React context available):

<SchemaView schema={schema} value={data} widgets={serverWidgets} />

Validation

<SchemaComponent
  schema={userSchema}
  value={user}
  onChange={setUser}
  validate
  onValidationError={(error) => console.error(error)}
/>

Validation uses the original Zod schema (if input was Zod) or z.fromJSONSchema() (if input was JSON Schema / OpenAPI).

Per-field validation errors

Add onValidationError to individual field overrides to receive errors for specific fields:

<SchemaComponent
  schema={userSchema}
  value={user}
  onChange={setUser}
  validate
  fields={{
    email: { onValidationError: (err) => setEmailError(err) },
    name: { onValidationError: (err) => setNameError(err) },
  }}
/>

Errors are dispatched based on Zod error paths. The root-level onValidationError still receives all errors.

Field visibility

Hide fields conditionally using the visible override:

<SchemaComponent
  schema={paymentSchema}
  value={payment}
  fields={{
    cardNumber: { visible: payment.method === "card" },
    sortCode: { visible: payment.method === "bank" },
  }}
/>

When visible: false, the field is completely removed — no label, no empty placeholder, no hidden input.

Field ordering

Control the order fields appear in rendered objects using order:

<SchemaComponent
  schema={userSchema}
  value={user}
  fields={{
    email: { order: 1 },
    name: { order: 2 },
    role: { order: 3 },
  }}
/>

Lower order values render first. Fields without order keep their insertion order and appear after ordered fields. Can also be set in schema metadata:

const schema = z.object({
  summary: z.string().meta({ order: 1 }),
  title: z.string().meta({ order: 2 }),
});

Discriminated unions

Discriminated unions (z.discriminatedUnion or JSON Schema oneOf with const properties) render as tabbed panels. Each tab is labelled by the discriminator's const value. Clicking a tab resets the value with the new discriminator.

const payment = z.discriminatedUnion("method", [
  z.object({
    method: z.literal("card"),
    cardNumber: z.string(),
    expiry: z.string(),
  }),
  z.object({
    method: z.literal("bank"),
    accountNumber: z.string(),
    sortCode: z.string(),
  }),
]);

<SchemaComponent schema={payment} value={{ method: "card", cardNumber: "4111...", expiry: "12/28" }} />

In read-only mode, only the active variant is rendered (no tabs).

Date and time inputs

String schemas with format: "date", format: "time", or format: "date-time" render as the corresponding HTML5 input types:

const eventSchema = z.object({
  date: z.string().meta({ format: "date" }),
  startTime: z.string().meta({ format: "time" }),
  createdAt: z.string().meta({ format: "date-time" }),
});

This produces <input type="date">, <input type="time">, and <input type="datetime-local"> respectively. In read-only mode, dates are formatted using toLocaleDateString() / toLocaleString().

Schema defaults

Default values from z.string().default("hello") or JSON Schema "default": "hello" are used when the value prop is undefined:

const schema = z.object({
  name: z.string().default("World"),
  count: z.number().default(0),
});

// Renders with "World" and 0 pre-filled
<SchemaComponent schema={schema} />

Defaults propagate through nested objects — each field uses its own default independently.

File uploads

String schemas with format: "binary" render as <input type="file">. Use contentMediaType to restrict accepted MIME types:

const schema = z.object({
  avatar: z.string().meta({ format: "binary" }),
  resume: z.string().meta({ format: "binary", contentMediaType: "application/pdf" }),
});

In read-only mode, file fields display a static label ("File field") since there is no value to show. The onChange callback receives the File object from the browser.

Server Components

For read-only rendering in a React Server Component, use <SchemaView>. It has zero hooks — no useContext, no useMemo, no useCallback — so it works without the "use client" directive.

import { SchemaView } from "schema-components/react/SchemaView";

export default async function Page() {
  const user = await getUser();
  return <SchemaView schema={userSchema} value={user} />;
}

SchemaView always renders read-only. For editable forms, use <SchemaComponent> (which requires "use client").

Pass the resolver explicitly since React context is unavailable in Server Components:

<SchemaView schema={schema} value={data} resolver={shadcnResolver} />

SchemaView produces identical output to <SchemaComponent readOnly> — verified by parity tests.

HTML rendering

Render schemas to HTML strings — no React needed. Useful for server-side rendering, email templates, static sites, and non-React environments.

import { renderToHtml } from "schema-components/html/renderToHtml";

const html = renderToHtml(userSchema, {
  value: { name: "Ada Lovelace", email: "[email protected]", role: "admin" },
  readOnly: true,
});

All HTML output uses sc- prefixed classes for styling hooks. HTML is properly escaped by the serialiser.

A default stylesheet is included:

<link rel="stylesheet" href="node_modules/schema-components/dist/html/styles.css">

Or import in JS:

import "schema-components/styles.css";

Streaming HTML

Three output formats for incremental rendering:

import { renderToHtmlChunks } from "schema-components/html/renderToHtmlStream";
import { renderToHtmlStream } from "schema-components/html/renderToHtmlStream";
import { renderToHtmlReadable } from "schema-components/html/renderToHtmlStream";

// Sync iterable — chunks yielded at field/item/entry boundaries
const chunks: string[] = [...renderToHtmlChunks(schema, { value })];

// Async iterable — yields control to event loop between chunks
for await (const chunk of renderToHtmlStream(schema, { value })) {
  res.write(chunk);
}

// Web ReadableStream — for Response, TransformStream, etc.
return new Response(renderToHtmlReadable(schema, { value }), {
  headers: { "Content-Type": "text/html" },
});

Structured HTML construction

The HTML renderer uses a typed h() builder instead of string templates:

import { h, serialize, raw } from "schema-components/html/html";

const input = h("input", { type: "text", id: "name", value: userValue });
serialize(input); // → <input type="text" id="name" value="Ada">

The builder handles void elements, boolean attributes, fragments, and nested children.

Accessibility

The HTML renderer produces WAI-ARIA-compliant markup:

| Attribute | When | |---|---| | id="<key>" | All editable inputs | | aria-required="true" | Required fields | | aria-describedby="<id>-hint" | Fields with constraints | | aria-readonly="true" | Read-only presentation spans | | aria-label="<description>" | Checkboxes | | role="group" | Record containers |

Error handling

Typed errors with onError callback for graceful degradation:

import { SchemaErrorBoundary } from "schema-components/react/SchemaErrorBoundary";

// Error boundary catches render errors from theme adapters
<SchemaErrorBoundary fallback={(error, reset) => <p>Error: {error.message}</p>}>
  <SchemaComponent schema={schema} value={data} />
</SchemaErrorBoundary>

// Per-component error callback
<SchemaComponent
  schema={schema}
  value={data}
  onError={(error) => {
    console.error(error);
    return null; // graceful degradation
  }}
/>

Without onError, errors re-throw. Error hierarchy: SchemaErrorSchemaNormalisationError | SchemaRenderError | SchemaFieldError.