No CSS (baseline)

Raw output from the library with no stylesheet loaded. Proves the HTML is semantic on its own — form controls, required markers, groups, and nested objects all work with browser defaults.

Used for sign-in and account recovery.
3-24 letters, numbers, or underscores.
Optional — link to your site or profile.
You can upgrade or downgrade any time.
Billing cycle
Only relevant for team plans.
Topics you care aboutPick at least one.
Profile
Up to 280 characters.
Rendered HTML
<form method="post" action="#">
  <div data-name="email" data-type="string" data-widget="email" data-required="true">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required="" aria-required="true" aria-describedby="email-hint"/>
    <small id="email-hint">Used for sign-in and account recovery.</small>
  </div>
  <div data-name="handle" data-type="string" data-widget="text" data-required="true">
    <label for="handle">Handle</label>
    <input id="handle" name="handle" type="text" required="" aria-required="true" aria-describedby="handle-hint" minlength="3" maxlength="24" pattern="^[a-zA-Z0-9_]+$" title="^[a-zA-Z0-9_]+$"/>
    <small id="handle-hint">3-24 letters, numbers, or underscores.</small>
  </div>
  <div data-name="homepage" data-type="string" data-widget="url">
    <label for="homepage">Homepage</label>
    <input id="homepage" name="homepage" type="url" aria-describedby="homepage-hint"/>
    <small id="homepage-hint">Optional — link to your site or profile.</small>
  </div>
  <div data-name="plan" data-type="string" data-widget="select" data-required="true">
    <label for="plan">Plan</label>
    <select id="plan" name="plan" required="" aria-required="true" aria-describedby="plan-hint">
      <option value="free">free</option>
      <option value="pro" selected="">pro</option>
      <option value="team">team</option>
    </select>
    <small id="plan-hint">You can upgrade or downgrade any time.</small>
  </div>
  <fieldset data-variant="group" data-name="billing" data-type="string" data-widget="radio">
    <legend>Billing cycle</legend>
    <label>
      <input type="radio" name="billing" value="monthly"/>
      <span>monthly</span>
    </label>
    <label>
      <input type="radio" name="billing" value="yearly" checked=""/>
      <span>yearly</span>
    </label>
  </fieldset>
  <div data-name="seats" data-type="integer" data-widget="range">
    <label for="seats">Seats</label>
    <input id="seats" name="seats" type="range" aria-describedby="seats-hint" value="5" min="1" max="50" step="1"/>
    <small id="seats-hint">Only relevant for team plans.</small>
  </div>
  <div data-name="notifications" data-type="boolean" data-widget="checkbox">
    <input id="notifications" name="notifications" type="checkbox" checked=""/>
    <label for="notifications">Email me product updates</label>
  </div>
  <fieldset data-variant="group" aria-required="true" aria-describedby="topics-hint" data-name="topics" data-type="array" data-widget="checkbox" data-required="true">
    <legend>Topics you care about</legend>
    <label>
      <input type="checkbox" name="topics" value="frontend" checked=""/>
      <span>frontend</span>
    </label>
    <label>
      <input type="checkbox" name="topics" value="backend" checked=""/>
      <span>backend</span>
    </label>
    <label>
      <input type="checkbox" name="topics" value="design"/>
      <span>design</span>
    </label>
    <label>
      <input type="checkbox" name="topics" value="ops"/>
      <span>ops</span>
    </label>
    <small id="topics-hint">Pick at least one.</small>
  </fieldset>
  <div data-name="channels" data-type="array" data-widget="select">
    <label for="channels">Notification channels</label>
    <select id="channels" name="channels" multiple="">
      <option value="email" selected="">email</option>
      <option value="in-app" selected="">in-app</option>
      <option value="sms">sms</option>
      <option value="push">push</option>
      <option value="rss">rss</option>
    </select>
  </div>
  <fieldset data-variant="object" data-name="profile" data-type="object" data-widget="object">
    <legend>Profile</legend>
    <div data-name="profile.name" data-type="string" data-widget="text">
      <label for="profile-name">Full name</label>
      <input id="profile-name" name="profile.name" type="text" minlength="1"/>
    </div>
    <div data-name="profile.bio" data-type="string" data-widget="textarea">
      <label for="profile-bio">Short bio</label>
      <textarea id="profile-bio" name="profile.bio" aria-describedby="profile-bio-hint" maxlength="280">
      </textarea>
      <small id="profile-bio-hint">Up to 280 characters.</small>
    </div>
  </fieldset>  <button type="submit">Create account</button>
</form>
examples/schema.ts
/**
 * A realistic sign-up / profile schema, shared across most demos to keep
 * them comparable: the only thing that changes between pages is the CSS.
 */
import type { ObjectSchema } from "../src/render.tsx";

export const schema: ObjectSchema = {
  type: "object",
  required: ["email", "handle", "plan", "topics"],
  properties: {
    email: {
      type: "string",
      format: "email",
      uiName: "Email",
      description: "Used for sign-in and account recovery.",
    },
    handle: {
      type: "string",
      minLength: 3,
      maxLength: 24,
      pattern: "^[a-zA-Z0-9_]+$",
      uiName: "Handle",
      description: "3-24 letters, numbers, or underscores.",
    },
    homepage: {
      type: "string",
      format: "uri",
      uiName: "Homepage",
      description: "Optional — link to your site or profile.",
    },
    plan: {
      type: "string",
      enum: ["free", "pro", "team"],
      default: "pro",
      uiWidget: "select",
      uiName: "Plan",
      description: "You can upgrade or downgrade any time.",
    },
    billing: {
      type: "string",
      enum: ["monthly", "yearly"],
      default: "yearly",
      uiName: "Billing cycle",
    },
    seats: {
      type: "integer",
      minimum: 1,
      maximum: 50,
      default: 5,
      uiWidget: "range",
      uiName: "Seats",
      description: "Only relevant for team plans.",
    },
    notifications: {
      type: "boolean",
      default: true,
      uiName: "Email me product updates",
    },
    topics: {
      type: "array",
      items: { type: "string", enum: ["frontend", "backend", "design", "ops"] },
      default: ["frontend", "backend"],
      uiName: "Topics you care about",
      description: "Pick at least one.",
    },
    channels: {
      type: "array",
      items: {
        type: "string",
        enum: ["email", "in-app", "sms", "push", "rss"],
      },
      default: ["email", "in-app"],
      uiWidget: "select",
      uiName: "Notification channels",
    },
    profile: {
      type: "object",
      uiName: "Profile",
      properties: {
        name: {
          type: "string",
          minLength: 1,
          uiName: "Full name",
        },
        bio: {
          type: "string",
          uiWidget: "textarea",
          maxLength: 280,
          uiName: "Short bio",
          description: "Up to 280 characters.",
        },
      },
    },
  },
};