Auto Form
Generate registry-native forms from Buf protobuf descriptors.
Made by malinskibeniaminInstallation
What is AutoForm
AutoForm is the schema-driven form platform for the registry.
It accepts Buf-generated protobuf descriptors or existing provider instances and renders a complete form with validation, field rendering, and payload management.
AutoForm v2 adds:
- Simple / Advanced / JSON modes
- an opt-in payload preview panel
- stable
testId-driven data-testid hooks for unit, integration, and e2e coverage - richer registry-native controls like InputGroup, Calendar, MultiSelect, RadioGroup, Toggle, and KeyValueField
- proto UI metadata plus UI CEL rules
- optional stepper flows
- payloadBuilder and payloadParser hooks for backend-shaped previews that still round-trip through JSON mode
Need proto-driven validation without auto-rendering? Use the useProtoForm hook for full control over field rendering with proto validation baked in.
Modes and summary
Use modes to switch between:
simple— only the minimum required surfaceadvanced— the full formjson— an editable payload editor
Enable showSummary to render a payload preview summary rail on wider screens. When space gets tight, it automatically drops below the form instead of squeezing the controls.
Testing hooks
AutoForm accepts a top-level testId prop and uses it as the form-wide prefix for stable data-testid attributes.
That gives you predictable selectors like:
project-launch-formproject-launch-form-field-project-nameproject-launch-form-field-project-name-controlproject-launch-form-tab-jsonproject-launch-form-summary
If you omit testId, AutoForm falls back to autoform, but for multi-form pages you should set an explicit prefix.
Field-type coverage
AutoForm auto-detects common patterns from proto field metadata:
- emails and URLs use InputGroup
- long text uses Textarea
- consent-style booleans use Checkbox
- regular booleans use Switch
- secret-like names use password inputs
- repeated enums use MultiSelect
- larger enums promote to Combobox
- bounded numeric fields use a slider + number input
Use fieldConfig.fieldType when you want an explicit control override (e.g., radio, toggle, currency, json, password, slider).
Protobuf usage
Protobuf support requires @bufbuild/protobuf v2 (protobuf-es v2) and @bufbuild/protovalidate v1. The older protobuf-es v1 API (MessageType, PlainMessage, class-based messages) is not compatible. If your project uses v1, you will need to migrate to v2 before using AutoForm with protobuf descriptors.
AutoForm accepts a generated Buf descriptor, not raw .proto text at runtime.
'use client';
import { AutoForm } from '@/components/auto-form';
import '@/lib/protobuf-provider/auto-form-example-annotations';
import {
type AutoFormExample,
AutoFormExampleSchema,
} from '@/lib/protobuf-provider/gen/auto-form-example_pb';
export function Example() {
return (
<AutoForm<AutoFormExample>
defaultMode="simple"
modes={['simple', 'advanced', 'json']}
schema={AutoFormExampleSchema}
showSummary
withSubmit
/>
);
}Proto UI metadata and CEL
The easiest way to think about this is:
- Protovalidate CEL handles validation
- AutoForm UI CEL handles visibility, disabled state, and step progression
So the proto schema declares the UI intent, protobuf-provider normalizes it, and AutoForm applies it at runtime.
The focused teaching fixture lives in:
packages/registry/src/lib/protobuf-provider/proto/auto-form-example.protopackages/registry/src/lib/protobuf-provider/proto/auto_form_ui.proto
Copy-paste proto example
message AutoFormUiMetadataExample {
option (redpanda.ui.registry.autoform.v1.message_ui).steps = {
id: "cluster"
title: "Cluster"
field_paths: "clusterName"
field_paths: "provider"
field_paths: "region"
field_paths: "enableSupportMode"
};
option (redpanda.ui.registry.autoform.v1.message_ui).steps = {
id: "support"
title: "Support"
field_paths: "supportTier"
field_paths: "maintenanceWindow"
field_paths: "escalationReason"
field_paths: "supportContact"
visible_when: {
expression: "form.enableSupportMode"
}
complete_when: {
expression: "!form.enableSupportMode || (form.supportTier != 0 && form.supportContact.case != '')"
message: "Choose a support tier and support contact before continuing."
}
};
UiDemoProvider provider = 2 [
(redpanda.ui.registry.autoform.v1.field_ui) = {
control: CONTROL_TYPE_RADIO_GROUP
}
];
string region = 3 [
(redpanda.ui.registry.autoform.v1.field_ui) = {
disabled_when: {
expression: "form.provider == 0"
}
}
];
bool enable_support_mode = 4 [
(redpanda.ui.registry.autoform.v1.field_ui) = {
control: CONTROL_TYPE_TOGGLE
}
];
string escalation_reason = 7 [
(redpanda.ui.registry.autoform.v1.field_ui) = {
control: CONTROL_TYPE_TEXTAREA,
visible_when: {
expression: "form.supportTier == 3"
}
}
];
oneof support_contact {
option (redpanda.ui.registry.autoform.v1.oneof_ui) = {
help: "This oneof only appears after a support tier is chosen."
visible_when: {
expression: "form.enableSupportMode && form.supportTier != 0"
}
};
string support_email = 8;
string slack_channel = 9;
bool no_follow_up = 10;
}
}What this gives you:
- the Support step only appears when support mode is enabled
regionstays disabled until a provider is selectedescalationReasononly appears for the highest support tier- the
supportContactoneof appears only after a support tier is chosen - Next stays blocked until the
complete_whenrule passes
Generated descriptor usage
'use client';
import { AutoForm } from '@/components/auto-form';
import '@/lib/protobuf-provider/auto-form-example-annotations';
import {
type AutoFormUiMetadataExample,
AutoFormUiMetadataExampleSchema,
} from '@/lib/protobuf-provider/gen/auto-form-example_pb';
export function Example() {
return (
<AutoForm<AutoFormUiMetadataExample>
modes={['advanced', 'json']}
schema={AutoFormUiMetadataExampleSchema}
showSummary
stepper
testId="proto-ui-metadata-form"
withSubmit
/>
);
}Interactive preview
Compact reference
| Option | Use it for |
|---|---|
message_ui.steps | Ordered step definitions, titles, descriptions, field paths, step visibility, and step completion rules |
field_ui.control | Preferred AutoForm control like radio, toggle, textarea, timestamp, or JSON |
field_ui.placeholder / example / help | User guidance that stays close to the generated field |
field_ui.visible_when | Hide or show a field from current form state |
field_ui.disabled_when | Keep a field visible but temporarily locked |
field_ui.step | Lightweight field-to-step assignment when you do not want message-level step definitions |
oneof_ui.help | Oneof-level helper copy for grouped selections |
oneof_ui.visible_when / disabled_when | Conditional rendering or disabling for the whole oneof |
oneof_ui.step | Lightweight oneof-to-step assignment |
CEL authoring notes
formis the full current form payloadthisis the current field value or step-local payload- keep UI CEL short and predictable
- use UI CEL for rendering and progression, not backend validation
Practical tips
- Prefer proto metadata over bespoke React conditionals when the rule belongs to the schema.
- Set an explicit
testIdso unit, integration, and e2e selectors stay stable. - Start from
AutoFormUiMetadataExamplewhen you need a small, copyable template for a new protobuf-backed form.
Stepper flows
Stepper support is optional.
- use
stepperto enable proto-declared steps - or pass
stepsdirectly for manual flows
If both are provided, the explicit steps prop wins.
<AutoForm
schema={AutoFormExampleSchema}
stepper
withSubmit
/>Payload preview hooks
Use payloadBuilder when the payload sent to your backend differs from raw form state.
Use renderSummary when you want the side panel to show something other than the default payload JSON preview.
If your payload shape differs from raw form state and you still want JSON-mode edits to sync back into the form, pair payloadBuilder with payloadParser.
<AutoForm
modes={['advanced', 'json']}
payloadBuilder={(values) => ({
request: {
owner: { email: values.ownerEmail },
rollout: {
mode: values.enableDryRun ? 'dry-run' : 'live',
region: values.targetRegion,
},
team: values.teamName,
},
})}
payloadParser={(payload) => fromRequestPayload(payload)}
schema={schema}
showSummary
withSubmit
/>Validation and error handling
For protobuf forms, AutoForm uses:
@bufbuild/protobuffor reflection and message generation@bufbuild/protovalidatefor validation- built-in UI CEL evaluation for visibility and step rules
- a built-in
createProtoResolver()bridge for React Hook Form
That gives you:
- field-level validation messages
- required
oneoffeedback - repeated/map validation
- message-level CEL errors surfaced in a top-level alert
Generated proto workflow
AutoForm expects the descriptor layer to be generated ahead of time.
- Write your
.proto - Generate protobuf-es output with Buf
- Optionally register comment annotations if you want docs-style descriptions from proto comments
- Import the generated
*Schemadescriptor into AutoForm
In this repo, generation is wired through:
packages/registry/buf.yamlpackages/registry/buf.gen.yamlpackages/registry/src/scripts/generate-proto-annotations.mts
Regenerate with:
bun --filter=@redpanda/registry run proto:generateAPI
schema
Accepts one of:
- a direct Buf
DescMessage - an existing provider instance
fieldConfig
Schema-agnostic UI overrides keyed by field path.
modes
Controls which tabs are available.
Default: ['advanced']
defaultMode
Controls the initial tab.
showSummary
Shows the live summary panel for form modes.
renderSummary
Optional custom renderer for the summary panel.
stepper
Opt-in switch for proto-defined stepper flows.
steps
Manual step definitions for custom flows.
payloadBuilder
Transforms current form values into the payload shown in the JSON tab and summary panel.
payloadParser
Optional inverse mapper for editable JSON mode when the payload shape differs from the form shape.
resolver
Optional override if you need custom resolver behavior.
AutoForm will automatically use createProtoResolver() for direct protobuf descriptors.
Supported protobuf field coverage
| Proto shape | AutoForm UI |
|---|---|
string | text / email / URL / password / textarea / currency presentations |
| numeric scalars | number input or slider + number input |
| 64-bit integers | string-backed integer input |
bool | tri-state select / checkbox / switch / toggle |
bytes | bytes editor (optional to hide in demos if that flow is too noisy) |
| enums | radio / select / combobox |
| nested messages | object section or JSON override |
| repeated fields | array editor / multiselect |
| maps | key/value editor |
oneof | case selector + active field editor |
google.protobuf.Timestamp | calendar + time controls |
google.protobuf.Duration | duration text input |
google.protobuf.FieldMask | paths editor |
Struct / Value / ListValue / Any | JSONField-backed fallback |
| wrapper types | optional scalar handlers |
Notes
- AutoForm protobuf support is Buf descriptor-based
- top-level JSON mode is editable
- summary panels are opt-in
- stepper flows are opt-in
- explicit React config wins over schema metadata
Recent changes
- patchv1.2.0Pin shipped dependency floors to the version we develop against. Registry items now declare ranges like `^5.1.9` (the actual installed version) instead of collapsing to `^5.0.0`, so consumers start on the known-tested b…#133
- minorv1.2.0Port the auto-form subsystem from ADP UI back to the registry.#122
- minorv1.1.0Theme docs refresh, readability pass on semantic foregrounds, and consumer-facing Base UI regression fixes.#121
- minorv1.0.0Post-Base-UI polish. Public API unchanged.#116
- majorv1.0.0Migrate every Radix-based primitive to `@base-ui/react@^1.4.0` (Base UI).#114