GolemUI in five claims
Form libraries make you choose: state management hooks where you write every input yourself, or schema-driven renderers locked to a single framework. GolemUI takes neither trade. One form definition, written with the TS API or the JSON API, produces the same form, with the same widgets and the same behavior, in React, Angular, Vue, Lit, and Vanilla JS.
A TS API built for composition
Section titled “A TS API built for composition”Forms are plain function calls returning plain data. That means everything you already do with code works on your forms: extract constants, spread shared props, map over your domain model, build fields with your own helper functions.
const required = { validator: { type: 'string', required: true } } as const;
const address = (prefix: string) => [ gui.inputs.dropdown(`${prefix}.country`, { label: 'Country', items: [ { label: 'Spain', value: 'es' }, { label: 'France', value: 'fr' }, ], ...required, }), gui.inputs.textInput(`${prefix}.city`, { label: 'City', ...required }), gui.inputs.textInput(`${prefix}.street`, { label: 'Street', ...required }), gui.inputs.textInput(`${prefix}.zip`, { label: 'ZIP code', ...required }),];
export const checkoutForm = [ ...address('shipping'), ...address('billing'), gui.actions.button({ label: 'Place order', actionType: 'submit' }),];No JSX trees to copy between frameworks, no template syntax to port. The definition is the single source of truth to share it, diff it, or generate it.
A JSON API with logic built in
Section titled “A JSON API with logic built in”When a definition has to live outside your code, you write it with the JSON API, validated by published JSON Schemas: autocompletion and inline errors in your editor, no plugins. And the JSON isn’t static: named states, conditional visibility, and per-state properties are part of the format.
{ "$schema": "https://golemui.com/schemas/form.schema.json", "states": { "newsletter": "$form.subscribe === true" }, "form": [ { "kind": "input", "type": "textinput", "path": "email", "label": "Get notified", "label.newsletter": "Get notified and subscribe to our newsletter", "validator": { "type": "string", "required": true, "format": "email" }, "props": { "hint": "We'll send you an email when we launch the product" } }, { "kind": "input", "type": "checkbox", "path": "subscribe", "label": "Subscribe to our newsletter" }, { "kind": "input", "type": "radiogroup", "path": "frequency", "label": "Newsletter frequency", "props": { "options": [ { "label": "Weekly", "value": "weekly" }, { "label": "Bi-weekly", "value": "bi-weekly" }, { "label": "Monthly", "value": "monthly" } ] }, "include": { "in": ["newsletter"] } } ]}Tick the checkbox and the form reacts: the newsletter state activates, the frequency field appears, and the email label updates itself. Suffix any property with a state name and it switches automatically. Validation lives in the definition too, so it travels with it.
Store form definitions in a database, serve them from an API, let a backend or an agent edit them. They’re data.
One definition, five runtimes, verified
Section titled “One definition, five runtimes, verified”The widgets are the same under every framework, so the output is identical by construction, not by discipline. We don’t promise parity, we test it: the same shared test suite runs against React, Angular, Vue, and Lit on every change.
Moving a form from your React app to your Angular admin means changing the mount point, not rewriting the form. And it composes across frameworks: the address() helper from above can live in a shared package and serve two different products.
// React app: user profileexport const editUserForm = [ gui.inputs.textInput('user.name', { label: 'Name' }), ...address('user.billing'),];
// Angular app: parcel shipping, same importexport const shippingForm = [ ...address('shipment.destination'), gui.inputs.datePicker('shipment.pickupDate', { label: 'Pickup date' }),];Same fields, same validation, same look, two frameworks. Fix the street validator once and both apps get it. Want countries populated from your API instead of a static list? Change the shared helper, in one place:
gui.inputs.dropdown(`${prefix}.country`, { label: 'Country', onLoad: (event) => { fetch('/api/countries') .then((r) => r.json()) .then((items) => event.update({ path: `${prefix}.country`, items })); }, ...required,}),Every form using address(), in every app, in every framework, now loads countries from the server. There is no per-framework data-fetching code to keep in sync.
AI-ready, with verification
Section titled “AI-ready, with verification”GolemUI ships an MCP server and agent skills that let coding agents build forms with either API and check their own work. Agent-generated JSON is validated against the real schemas; agent-generated TypeScript is type-checked against the library’s actual type declarations. The MCP refuses to approve code it can’t verify, so agents can’t hallucinate an API that doesn’t exist.
Say an agent invents a plural validators array that doesn’t exist:
gui.inputs.textInput('email', { label: 'Email', validators: [{ required: true, format: 'email' }],});dx_check_code rejects it with the real compiler diagnostic:
{ "ok": false, "diagnostics": [ { "code": 2769, "message": "... 'validators' does not exist in type 'Partial<Omit<TextDataInputDecorator, \"type\">>'. Did you mean to write 'validator'?", "line": 2 } ]}The agent fixes it and re-checks. No hallucinated API ever reaches your codebase.
It also generates. Point it at a JSON Schema or an OpenAPI operation and get a working form back, with a report of anything it couldn’t map. Given the request body of a typical shipping endpoint, POST /shipments/{id}/method:
type: objectrequired: [method]properties: method: type: string title: Shipping method enum: [standard, express, priority] signatureRequired: type: boolean title: Signature on delivery instructions: type: string title: Delivery instructionsjson_generate_from_openapi returns a validated form definition. The enum becomes a select with its values enforced by the validator:
{ "kind": "input", "type": "select", "path": "method", "label": "Shipping method", "validator": { "type": "string", "required": true, "enum": ["standard", "express", "priority"] }, "props": { "options": [ { "label": "Standard", "value": "standard" }, { "label": "Express", "value": "express" }, { "label": "Priority", "value": "priority" } ] }}The boolean becomes a checkbox, the optional string a plain text input, and a submit button is added at the end. The response closes with "diagnostics": [] and a validation report. An empty diagnostics list tells the agent, or your CI, that the form expresses the whole schema.
Extensible where it matters
Section titled “Extensible where it matters”GolemUI means freedom to choose. We know that every project is a world of its own, and what fits perfectly in one can be the wrong call in the next. We ship defaults so you can start in minutes: a complete widget set, Zod-backed validators, packaged themes. But every one of them is a default, not a cage:
- UI kit. Swap any widget for your own component, or bring an entire widget set from your design system.
- Validation. Validators speak Standard Schema, so you can plug in your own Zod, Valibot, or ArkType schemas.
- i18n. Bring your own library, i18next or anything else, with hot language switching and automatic RTL either way.
- Styling. Use a packaged theme, restyle through CSS custom properties on light-DOM widgets your stylesheet can always reach, or skip our CSS entirely and go headless. Widgets stay functional and accessible no matter what you do with the styles.
- Dependencies. Widgets that need a third-party helper, like a markdown parser, don’t bundle one: you inject the implementation you prefer.
- Authoring. The
dxpipeline that powersgui.*is public: use it to build your own typed generator functions and selector chains for your own widget set.
Open Source. Forever.
Section titled “Open Source. Forever.”Open source, for us, means trust: anyone can read the code, audit it for security, report issues, and help shape what comes next.
GolemUI will always be open source. Forever.
Next steps
Section titled “Next steps”Everything above is one install away. If you would rather start with the story, Why GolemUI on the blog tells it through a project you may have lived through.
Get started GitHub