Forms, switch product
opinion

Why GolemUI

Imagine defining a dynamic, declarative, translatable form once, in code or in JSON, without committing to a framework or a UI library, and validating it with the Standard Schema validator you already use. Stop imagining. That is GolemUI.

That is a lot to claim in one sentence, so the rest of this article makes the case in detail. It starts with a project you may have lived through, and ends with the cases where you may not need GolemUI at all.

A new project starts, and the requirements are simple: a login screen, a profile page, a checkout. You pick a form library, a validation library, and the component library your team already uses. Everything is typed, fast, and static. It works, and it works well.

Month six arrives. The product manager brings a new requirement: “We need a questionnaire where the backend decides which questions appear, based on the previous answers.”

So you invent a JSON format for the backend to send. You write a <DynamicForm/> component that maps over it. Inside it, a switch statement picks a component for each field.type, and an in-house showWhen mechanism handles conditional visibility.

Month twelve: validation messages now have to be shown in four languages, so the JSON format grows a translation layer. Month eighteen: the design team replaces the component library, and a second app, written in another framework, needs the same questionnaire.

None of these steps was a mistake. Each one was the reasonable next step. That is exactly how it happens: you are now maintaining an in-house form engine. It is undocumented, coupled to one framework and one set of components, and tested only in the places that have already failed once.

Every framework already has form libraries that developers trust, so the honest answer starts with what GolemUI does not do. It does not manage the state of inputs you wrote by hand. For forms whose fields are known when you write the code, the library you already use is mature, fast, and well designed. If that is your situation, what you have may be all you need.

The difference is what a form is made of. In a classic form library, a form is code: JSX or a template you write field by field. In GolemUI, a form is data : a serializable definition that a runtime renders. The moment a form’s shape is decided at runtime (by a server, a CMS, a database, or an AI model), code stops being a workable format, and teams respond by building a rendering engine on top of their state library.

That engine is the expensive part. The widget registry, the conditional visibility rules, the validation wiring, the translations, the accessibility work: none of it comes from the state library underneath. GolemUI is that engine, already built, MIT licensed, and maintained as a product rather than as an internal tool nobody has time for.

This is what the month-six requirement usually turns into:

// The field format you invent for the backend to send:
const questionnaire = [
{
id: 'employment',
type: 'radio',
label: 'Employment status',
options: ['Employed', 'Self-employed', 'Unemployed'],
required: true,
},
{
id: 'companyName',
type: 'text',
label: 'Company name',
required: true,
showWhen: { field: 'employment', equals: 'Employed' },
},
];
// And the engine that renders it. One branch per widget type, and this
// is the short version: the condition evaluator, the validation wiring,
// the translation layer, and the accessibility work are still missing.
function DynamicForm({ fields, formValues, onSubmit }) {
return (
<form onSubmit={onSubmit}>
{fields.map((field) => {
if (field.showWhen && !evaluateCondition(field.showWhen, formValues)) {
return null;
}
switch (field.type) {
case 'radio': {
return <RadioField key={field.id} {...field} />;
}
case 'text': {
return <TextField key={field.id} {...field} />;
}
case 'number': {
return <NumberField key={field.id} {...field} />;
}
// ...one case for every widget the product will ever need
default: {
return null;
}
}
})}
<button type="submit">Continue</button>
</form>
);
}

Every team writes a different version of this component, and every version grows the same way: more cases, more props, more special rules that only one person understands.

Here is the same questionnaire in GolemUI, written with the TS API and shown in React:

import type { FormSubmitEvent } from '@golemui/core';
import { GuiForm } from '@golemui/gui-react';
import { gui } from '@golemui/gui-shared';
const questionnaire = [
gui.inputs.radiogroup('employment', {
label: 'Employment status',
options: ['Employed', 'Self-employed', 'Unemployed'],
validator: { type: 'string', required: true },
}),
gui.inputs.textInput('companyName', {
label: 'Company name',
validator: { type: 'string', required: true },
include: { when: '$form.employment === "Employed"' },
}),
gui.actions.button({
label: 'Continue',
actionType: 'submit',
disabled: { when: '$formIsInvalid' },
}),
];
function handleSubmit(event: FormSubmitEvent) {
console.log(event.data);
}
export function Questionnaire() {
return (
<GuiForm config={{ formDef: questionnaire }} formSubmit={handleSubmit} />
);
}

Three things are worth noticing. First, there is no branching: conditional visibility is part of the definition, declared with include and exclude rules and evaluated live against the form data. Conditions are expression strings rather than functions on purpose, because strings serialize and functions do not.

Second, the definition is a plain array of function calls, and it composes like any other code: shared fragments spread into larger forms, constants carry the options you repeat, and application code can generate fields from data instead of writing them by hand.

Third, the engine holds the form data itself. Widgets write into it as the user types, events report every change to your code, formSubmit delivers the validated result, and runtime methods push data in from outside.

A definition does not have to be written in code at all. The TS API is for forms that live in your code, with autocompletion, type checking, and access to runtime functions that JSON cannot express. When the definition has to live somewhere else (stored in a database, versioned and sent by a backend, generated by a tool), you write it with the JSON API instead, as schema-validated JSON:

{
"$schema": "https://golemui.com/schemas/form.schema.json",
"form": [
{
"kind": "input",
"type": "radiogroup",
"path": "employment",
"label": "Employment status",
"props": { "options": ["Employed", "Self-employed", "Unemployed"] },
"validator": { "type": "string", "required": true }
},
{
"kind": "input",
"type": "textinput",
"path": "companyName",
"label": "Company name",
"include": { "when": "$form.employment === 'Employed'" },
"validator": { "type": "string", "required": true }
},
{
"kind": "action",
"type": "button",
"label": "Continue",
"actionType": "submit",
"disabled": { "when": "$formIsInvalid" }
}
]
}

The backend stores that JSON, versions it, and sends it. The frontend passes it to the same config prop, and the engine treats both formats as the same definition. That is the month-six requirement, met without building anything.

The definition above is not an illustration. It is a file this site serves, and the engine renders it here, live. Pick “Employed” to reveal the follow-up question, and leave that field empty to see the Continue button disable itself until the form is valid:


The two APIs are a choice you make per form. Pick the JSON API when the definition has to live outside your code, and the TS API when it lives inside. The MCP server takes the generated case further: AI assistants can produce definitions from a JSON Schema or an OpenAPI spec and validate the result before it ever reaches your review. We measured that workflow across React, Vue, and Angular, and across three models: the AI-built GolemUI forms matched the conventional stacks on correctness, in about a third of the code.

The same definition renders in React, Angular, Vue, and Lit, plus vanilla JS through the <gui-form> custom element. These are thin adapters over one core, not four separate implementations. If your organization mixes frameworks, or migrates between them, the forms move without a rewrite. In the in-house engine story, this was the second app at month eighteen.

GolemUI ships with a built-in set of more than 30 accessible widgets themed through CSS custom properties. You can replace individual widgets with your own, or, since v1.2, package your design system as a complete widget set with its own typed namespace, using the same pipeline that builds the built-in widgets themselves. In the in-house engine story, this was the component library replacement, also at month eighteen.

Labels, hints, and validation messages go through an adapter you connect to i18next, Lingui, or your own system. Languages switch live, and right-to-left layouts are handled automatically. In the in-house engine story, this was the month-twelve rewrite.

The built-in validators cover the common rules, and they are Standard Schema compliant. So is the integration point: any Standard Schema validator works directly, which means Zod, Valibot, and ArkType connect without adapter packages.

If your forms are static and their fields are known at compile time (a login page, a contact form, a settings panel), GolemUI might not be the ideal tool. The form library you already know is smaller, more direct, and excellent at exactly that. A static form written with the TS API is still a fully supported use, and teams that prefer writing every form the same way choose it deliberately. For them the engine is a convenience. For data-driven forms it is the point.

It is also worth knowing what GolemUI costs before you adopt it:

  • It is an abstraction with a runtime. You describe forms instead of writing their markup, and that indirection is easiest to justify when the form’s shape is data.
  • Conditional logic is written as expressions, which keeps definitions serializable. When you need real code, runtime functions accept a callback in place of a props object, and that widget gives up serializability in exchange.
  • The default widgets are custom elements rendered by Lit. Your framework sees them as DOM elements rather than as its own components, and Lit is part of the runtime you ship.
  • It is young. You will find thorough documentation and an active team, but not ten years of answered questions. The code, changelog, and roadmap are public if you want to see how the project is run before adopting it.

And the cases where it is the right tool are equally concrete:

  • The backend, a CMS, or an AI model decides what the user sees.
  • Questionnaires, surveys, onboarding, and know-your-customer flows.
  • Admin panels and internal tools generated from data models.
  • The same forms running in more than one framework.
  • Forms that must be stored, versioned, reviewed, or generated.

GolemUI exists because we kept building the same engine, project after project, and decided the next one should be the last. Any library can make claims like the ones in this article. The docs make them concrete, point by point, in GolemUI in five claims. Still, the only reliable way to judge a form library is to build a form with it.

Get started GitHub
Previous GolemUI v1.2.0: bring your own widget set Next GolemUI v1.3.0: JSON schemas for your own widget set