---
title: Auto Form
description: Generate registry-native forms from Buf protobuf descriptors.
icon: zap
author:
  name: malinskibeniamin
  url: https://github.com/malinskibeniamin
mode: wide
---

<PoweredBy
  packages={[
    { name: 'react-hook-form', url: 'https://react-hook-form.com/' },
    { name: '@bufbuild/protobuf', url: 'https://github.com/bufbuild/protobuf-es' },
    { name: '@bufbuild/protovalidate', url: 'https://buf.build/docs/protovalidate/' },
  ]}
/>

### Installation

Configure the [public Buf package registry](/docs/install#protobuf-and-structured-error-dependencies)
before installing the protobuf dependencies.

```package-install
npx 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

<ComponentPreview name="auto-form-protobuf-complex-demo" />

:::info
Need proto-driven validation without auto-rendering? Use the [`useProtoForm`](/docs/use-proto-form) 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 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

:::warning
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](https://github.com/bufbuild/protobuf-es/blob/main/MANUAL.md#migrating-from-version-1) before using AutoForm with protobuf descriptors.
:::

AutoForm accepts a **generated Buf descriptor**, not raw `.proto` text at runtime.

```tsx lineNumbers
'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
    />
  );
}
```

<ComponentPreview name="auto-form-protobuf-complex-demo" />

## 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 lineNumbers expandable
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 lineNumbers
'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

<ComponentPreview name="auto-form-protobuf-ui-metadata-demo" />

### 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 lineNumbers
<AutoForm schema={AutoFormExampleSchema} stepper withSubmit />
```

<ComponentPreview name="auto-form-stepper-demo" />

## 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 lineNumbers
<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

<ComponentPreview name="auto-form-protobuf-validation-demo" />

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](#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
