Auto Form
Generate registry-native forms from Buf protobuf descriptors.
Installation
Configure the public Buf package registry before installing the protobuf dependencies.
npx shadcn@latest add @redpanda/auto-formpnpm dlx shadcn@latest add @redpanda/auto-formyarn dlx shadcn@latest add @redpanda/auto-formbunx shadcn@latest add @redpanda/auto-formnubx shadcn@latest add @redpanda/auto-formaube dlx shadcn@latest add @redpanda/auto-formWhat 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
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
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 />Custom content slots
Use explicit slots descriptors to insert custom content without inspecting or rewriting ordinary children:
<AutoForm schema={AutoFormExampleSchema} slots={[ { id: 'intro', placement: 'start', content: <p>Complete the account details below.</p> }, { id: 'username-help', placement: 'before', field: 'username', content: <p>The username is included in the submitted payload.</p>, }, ]}/>Each descriptor needs a stable, unique, nonblank id; do not derive identity from array position or displayed content. placement is start, before, or after. Field placements require a field matching a root schema key, not a nested path. Descriptors at the same placement appear in array order, and their IDs preserve local state while that placement remains mounted.
A start slot appears once in the current form mode or active step panel, not once for every configured step. A field-anchored slot appears only while its target field is in that mode or step; an advanced-only anchor is valid but hidden in simple mode. Slots do not appear in JSON mode. Invalid identities or unknown root anchors produce a visible configuration error instead of a usable form underneath it. Supply a corrected new slots array to retry without changing the schema; ordinary valid slot-array updates do not reset form state.
Ordinary children remain untouched: they appear before fields in a non-stepper form and after navigation controls in a stepper. The former AutoFormSlot marker component is removed; migrate its content and before/after placement to a descriptor.
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
Built-in controls use their primitive’s invalid and disabled recipes. Field masks keep monospace text and accept one path per line. The optional toggle control has one focusable Switch and a noninteractive On/Off status badge, not a second toggle button. Field help supports keyboard focus and pointer hover; validation errors stay visible beside the field rather than only inside a tooltip.
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
formRef
Optional React ref to the live React Hook Form methods. Its validated output keeps the form’s
generic type. Read the ref from event handlers; React clears it when the form unmounts. Use
formProps.ref when you need the native form element instead.
The former onFormInit callback is removed. Do not copy form methods into parent state from an
initialization effect; use this ref for imperative actions, and the existing submit or field-change
callbacks for their corresponding events.
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.
slots
Readonly AutoFormSlotDescriptor array for explicitly keyed start/before/after content. See custom content slots for placement, identity, and mode/step behavior.
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