@mackehansson/schemaform

Layout

Shape a form with Pages, Groups, Columns, Controls, and nested object scopes.

The UI Schema is Schemaform's presentation layer: a layout tree of Pages, layout nodes, and Controls. It decides where fields appear, while JSON Schema still owns data shape and validation.

Pages

Every UI Schema root has pages: [...]. A one-page form uses a one-element array; a multi-page form adds more Page nodes. A Page is purely presentational. One JSON Schema spans all pages, and the Submission Payload is produced from the whole visible form.

uiSchema: {
  pages: [
    { type: "Page", title: "Profile", children: [] },
    { type: "Page", title: "Address", children: [] },
  ],
}

Groups and Columns

Use Group nodes for titled sections and Columns nodes for equal-width side-by-side layout. Both are layout nodes: they can contain Controls or other layout nodes, and they can be targeted by Rules through an id.

{
  type: "Group",
  id: "name-group",
  title: "Name",
  children: [
    {
      type: "Columns",
      id: "name-columns",
      children: [
        { type: "Control", scope: "#/properties/firstName" },
        { type: "Control", scope: "#/properties/lastName" },
      ],
    },
  ],
}

Nested object mapping

A Control points to one JSON Schema property with a JSON Pointer scope. Nested objects keep walking through properties:

{ type: "Control", scope: "#/properties/address/properties/street" }

That Control resolves to the Working State path address.street and the validation pointer /address/street. The object itself is still modeled in JSON Schema; UI Schema only chooses where its child Controls render.

Live example

This example uses two Pages, Groups for sections, Columns for compact rows, and nested object Controls for the address fields.

Live form

Profile

Name

Source

import type { FormDefinition } from "@mackehansson/schemaform";import { SchemaForm } from "@mackehansson/schemaform-shadcn";const onboardingForm = {  formatVersion: 1,  schema: {    type: "object",    required: ["firstName", "lastName", "address"],    properties: {      firstName: { type: "string", title: "First name", minLength: 1 },      lastName: { type: "string", title: "Last name", minLength: 1 },      email: { type: "string", title: "Email", format: "email" },      address: {        type: "object",        title: "Address",        required: ["street", "city"],        properties: {          street: { type: "string", title: "Street" },          city: { type: "string", title: "City" },          postalCode: { type: "string", title: "Postal code" },        },      },      notes: { type: "string", title: "Notes" },    },  },  uiSchema: {    pages: [      {        type: "Page",        id: "page-profile",        title: "Profile",        children: [          {            type: "Group",            id: "name-group",            title: "Name",            children: [              {                type: "Columns",                id: "name-columns",                children: [                  { type: "Control", scope: "#/properties/firstName" },                  { type: "Control", scope: "#/properties/lastName" },                ],              },              { type: "Control", scope: "#/properties/email" },            ],          },        ],      },      {        type: "Page",        id: "page-address",        title: "Address",        children: [          {            type: "Group",            id: "address-group",            title: "Mailing address",            children: [              { type: "Control", scope: "#/properties/address/properties/street" },              {                type: "Columns",                id: "address-columns",                children: [                  { type: "Control", scope: "#/properties/address/properties/city" },                  { type: "Control", scope: "#/properties/address/properties/postalCode" },                ],              },            ],          },          {            type: "Control",            scope: "#/properties/notes",            widget: "textarea",          },        ],      },    ],  },  rules: [],} satisfies FormDefinition;export function App() {  return (    <SchemaForm      definition={onboardingForm}      onSubmit={(data) => console.log(data)}    />  );}

On this page