> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tappify.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Build forms from schemas

> Render a JSON Schema as a host-styled form with validation, instead of writing inputs by hand.

`TapForm` renders the same schema your manifest already declares — for a settings panel, an
action's input, or a credential set — as a form in the host's styling, validates on submit,
and hands you a typed value.

```tsx theme={null}
import { TapForm } from "@tappify/extension-sdk";
import schema from "../schemas/settings.json";

<TapForm
  schema={schema}
  value={values}
  submitLabel="Save"
  onSubmit={(next) => onChange(next)}
/>;
```

## Props

| Prop          | Type                                  | Means                                                                                                 |
| ------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `schema`      | `JsonSchema`                          | The object schema to render. One control per declared property                                        |
| `value`       | `Partial<T>`                          | The values the fields start at, and later changes to it do not reset the form. Omit for an empty form |
| `submitLabel` | `string`                              | Label on the submit button. Defaults to `Submit`                                                      |
| `cancelLabel` | `string`                              | Label on the cancel button. Defaults to `Cancel`                                                      |
| `busy`        | `boolean`                             | Disables the submit button and marks it busy while a submit is in flight                              |
| `onSubmit`    | `(value: T) => void \| Promise<void>` | Called with the validated value                                                                       |
| `onCancel`    | `() => void`                          | Renders a cancel button, and is called by it                                                          |

## What each schema construct renders

| Schema                                         | Control                                                                      |
| ---------------------------------------------- | ---------------------------------------------------------------------------- |
| `"type": "string"`                             | Single-line input                                                            |
| `"type": "string"` with `"format": "textarea"` | Textarea                                                                     |
| `"type": "string"` with `"format": "date"`     | Date input                                                                   |
| `"enum"` on a property                         | Select, one option per value                                                 |
| `"type": "number"` or `"integer"`              | Number input                                                                 |
| `"type": "boolean"`                            | Checkbox                                                                     |
| `title`                                        | The control's label. Falls back to the property name, spaced and capitalised |
| `description`                                  | The hint under the control                                                   |
| `required`                                     | Marks the control required and blocks submit when empty                      |

Submit drops the fields left blank, then checks what remains against the whole schema, so a
`minimum`, a `pattern` or a `maxLength` you declared is enforced too and its message lands
under the control it belongs to.

`formFields(schema)` returns the same list `TapForm` renders, as
`{ name, label, kind, required, hint, options }`, when you want the fields without the form.

## An action behind a form

An action goes through an approval card, so the form collects the input and the host does the
rest:

```tsx theme={null}
import { TapForm, useTap, type ActionInput } from "@tappify/extension-sdk";
import schema from "../schemas/send-push.input.json";

export function SendPush() {
  const tap = useTap();

  return (
    <TapForm
      schema={schema}
      submitLabel="Send"
      onSubmit={(input: ActionInput<"send-push">) => {
        void tap.actions.run("send-push", input);
      }}
    />
  );
}
```

`tap.actions.run` posts the values, and Tappify draws the approval card — the scope, the blast
radius, whether it can be undone and the cost — before anything runs. See
[Add actions](/extensions/build/add-actions).

A form with no `value` has nothing to infer `T` from, so annotate the handler's parameter —
`ActionInput<"send-push">` here, `TapSettingsValues` in a settings panel. `onSubmit` returns
nothing, so hand the promise to `void` rather than returning it.

Point `TapForm` at the same file your manifest references. When the schema changes,
[`tappify extension types`](/extensions/build/generated-types) changes the type in the same
step, and a form that no longer matches its handler stops compiling.
