# Form dialog (goodtabs UI)

A dialog holding a validated form that returns focus to its trigger on close.

## Install

```bash
npx shadcn@latest add https://ui.goodtabs.co/r/form-dialog.json
```

Or, with the @goodtabs namespace configured in components.json: `npx shadcn@latest add @goodtabs/form-dialog`

## Usage

```tsx
import { FormDialog } from "@/components/ui/form-dialog"

<FormDialog
  title="Invite a teammate"
  fields={[{ name: "email", label: "Work email", type: "email", required: true }]}
  onSubmit={async (v) => { await invite(v.email) }}
>
  <Button>Invite</Button>
</FormDialog>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| children | `ReactNode` |  | The trigger element, rendered with asChild. |
| title | `string` |  | What the form does, as a short phrase. |
| description | `string` |  | One line of context under the title. |
| fields | `FormDialogField[]` |  | name, label, type, placeholder, hint, required and an optional validate function. |
| submitLabel | `string` | `"Save"` | Label of the submit button. |
| cancelLabel | `string` | `"Cancel"` | Label of the cancel button. |
| onSubmit | `(values: Record<string, string>) => void | Promise<void>` |  | Runs once every field passes. A promise shows a pending state; a throw keeps the dialog open. |

## When to use

- Collecting two or three values without leaving the page
- Create or rename flows where a failed submit must keep the input
- Any short form that needs a pending and an error state

## When not to use

- Yes or no questions, use the confirm dialog
- Long forms or multi-step flows, give them a page
- Anything that needs fields other than text, email or url, compose Dialog yourself

## Notes for agents

- onSubmit and validate are functions, so the component that renders FormDialog must be a client component
- Fields are uncontrolled and read from FormData on submit; values are trimmed
- Validation runs on submit, focus moves to the first invalid input, then errors re-check as the user types
- Radix returns focus to the trigger on close; the dialog has no trigger-less mode, so wrap it around the element that opens it
- While onSubmit is pending, Escape and Cancel are locked
- Errors use text-destructive plus aria-invalid and role=alert, never colour alone

Docs: https://ui.goodtabs.co/components/form-dialog