Skip to content
Redpanda UI
Esc
↑↓navigate↵open⌘Jpreview
Made by malinskibeniamin
Edit on GitHub

Auto Form

Generate registry-native forms from Buf protobuf descriptors.

Installation

Configure the public Buf package registry before installing the protobuf dependencies.

bash
npx shadcn@latest add @redpanda/auto-form
bash
pnpm dlx shadcn@latest add @redpanda/auto-form
bash
yarn dlx shadcn@latest add @redpanda/auto-form
bash
bunx shadcn@latest add @redpanda/auto-form
bash
nubx shadcn@latest add @redpanda/auto-form
bash
aube dlx shadcn@latest add @redpanda/auto-form

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

Modes and summary

Use modes to switch between:

  • simple — only the minimum required surface
  • advanced — the full form
  • json — 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-form
  • project-launch-form-field-project-name
  • project-launch-form-field-project-name-control
  • project-launch-form-tab-json
  • project-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.

tsx
'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.proto
  • packages/registry/src/lib/protobuf-provider/proto/auto_form_ui.proto

Copy-paste proto example

proto
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
  • region stays disabled until a provider is selected
  • escalationReason only appears for the highest support tier
  • the supportContact oneof appears only after a support tier is chosen
  • Next stays blocked until the complete_when rule passes

Generated descriptor usage

tsx
'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

  • form is the full current form payload
  • this is 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 testId so unit, integration, and e2e selectors stay stable.
  • Start from AutoFormUiMetadataExample when you need a small, copyable template for a new protobuf-backed form.

Stepper flows

Stepper support is optional.

  • use stepper to enable proto-declared steps
  • or pass steps directly for manual flows

If both are provided, the explicit steps prop wins.

tsx
<AutoForm schema={AutoFormExampleSchema} stepper withSubmit />

Custom content slots

Use explicit slots descriptors to insert custom content without inspecting or rewriting ordinary children:

tsx
<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.

tsx
<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/protobuf for reflection and message generation
  • @bufbuild/protovalidate for 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 oneof feedback
  • 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.

  1. Write your .proto
  2. Generate protobuf-es output with Buf
  3. Optionally register comment annotations if you want docs-style descriptions from proto comments
  4. Import the generated *Schema descriptor into AutoForm

In this repo, generation is wired through:

  • packages/registry/buf.yaml
  • packages/registry/buf.gen.yaml
  • packages/registry/src/scripts/generate-proto-annotations.mts

Regenerate with:

bash
bun --filter=@redpanda/registry run proto:generate

API

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

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 baseline while caret semantics still allow any compatible release within the same major.#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
See full history →

Last updated on

Was this page helpful?