# UI (https://form.dimah.dev/docs/ui)



`@dimah-form/ui` is **optional**. `@dimah-form/react` stays headless. The renderer takes a `useFormResponse` return as `form` — it does not call the hook.

Built-in types (`text`, `email`, `date`, `number`, `boolean`, `select`, `multiSelect`) have widgets. Custom `defineFieldType` types register once on `FormUiProvider` with the same `type` string — or a `meta.widget` key to swap one field without replacing the type.

***

## Install [#install]

npm:

```bash
pnpm add @dimah-form/ui @dimah-form/react
```

```css
@import "@dimah-form/ui/styles.css";
```

Or copy source into your app with the shadcn CLI (HTTP registry after docs deploy, or GitHub):

```bash
pnpm dlx shadcn@latest add https://form.dimah.dev/r/form.json
pnpm dlx shadcn@latest add dimah-kz/dimah-form/form
```

***

## Template [#template]

```tsx
import { FormUiProvider, FormView } from "@dimah-form/ui";

const widgets = { rating: StarRatingField, "text.mask": MaskedTextField };
const form = useFormResponse({ snapshot });

return (
  <FormUiProvider translations={fa} widgets={widgets}>
    <FormView form={form} />
  </FormUiProvider>
);
```

`FormView` is the default layout. `layout="auto"` (default) uses a wizard when `meta.step` spans more than one page on the snapshot, and widgets with `mode="review"` when the response is locked. Pass `layout="fill" | "steps" | "review"` — or `form.meta.layout` — to force one. Compact `FormReview` (shadcn `Item` list of formatted answers) is opt-in: `review={<FormReview />}` or `components={{ Review: FormReview }}`.

Slot props take `false` to hide, a node to replace, or a function to wrap the default (`header={({ default: node }) => <Card>{node}</Card>}`): `header`, `progress`, `status`, `errorSummary`, `error`, `saveState`, `actions`, `stepList`, `stepHeading`, `stepNav`, `review`, `inactive`. The default header includes the save-state badge. Stepped layouts show `FormStepList` and omit the progress bar and step heading unless you pass `stepList={false}` (then you get heading + step progress). Pass `progress={<FormProgress variant="required" />}` to keep a completion bar in a wizard.

`render` replaces the whole composition — keep `FormRoot` so Enter / submit still work:

```tsx
<FormView
  form={form}
  header={false}
  render={({ fields, actions, error, saveState }) => (
    <FormRoot>
      <Card>
        {fields}
        {error}
        {saveState}
        {actions}
      </Card>
    </FormRoot>
  )}
/>
```

Failed submit (and failed Next) focuses the first invalid control. Enter on a non-last step advances instead of submitting.

Required labels render an asterisk. Pass your own mark — or the whole field frame — on the provider:

```tsx
function RequiredMark() {
  return (
    <span className="ms-1 text-dimah-form-destructive" aria-hidden>
      required
    </span>
  );
}

<FormUiProvider
  components={{ RequiredMark, FieldFrame: MyFieldFrame }}
  formatIssue={(binding) =>
    binding.errorCode === "TOO_SMALL" ? "Pick a higher score" : undefined
  }
>
  <FormView form={form} />
</FormUiProvider>;
```

Custom widgets should wrap the control in `FormFieldFrame` so `components.FieldFrame` applies.

***

## Compose [#compose]

Every chrome piece is a primitive. `FormScope` provides the session so they do not need a `form` prop:

```tsx
<FormScope form={form}>
  <FormRoot>
    <FormHeader />
    <FormProgress />
    <FormFields />
    <FormActions before={<CancelButton />} />
  </FormRoot>
</FormScope>
```

* `FormField` — looks up `meta.widget` (when registered and not a built-in `radio` / `switch` / `chips` variant) then `field.type`. Pass `children` to replace that widget. Widgets receive `mode: "edit" | "review"`.
* `FormFieldFrame` — label / description / help / issue chrome. `layout="stack"` (default), `"choice"` (checkbox), or `"group"` (fieldset). `data-field-type`, `data-widget`, `data-required`, `data-invalid` are set for CSS hooks.
* `FormFields` — maps `visibleFields`. `fields` / `filter` restrict the loop; `meta.section` groups into `FormSection` (or `groupBy={false}` to opt out). `meta.width` `"half"` / `"third"` shares a row. `renderField={(binding, { defaultField }) => …}` keeps the loop.
* `FormProgress` / `FormSaveState` / `FormReview` — shadcn `Progress` (`auto` is wizard page when inside `FormSteps`, otherwise required-field completion), `Badge` for dirty/saving/saved drafts, compact `Item` list (`renderValue` to swap one row). Locked `FormView` review uses `FormFields` / widgets (`mode="review"`) unless you opt into `FormReview`.
* `FormHeader` — title / description. Omit `saveState` for `FormSaveState` (hidden while locked); `components.Header` / `components.SaveState` swap either piece.
* `FormErrorSummary` — links to invalid fields after validate/submit.
* `FormSteps` — groups snapshot `meta.step` (number or string, default `"1"`). Hidden fields drop out of the page; the current page is a **key**, not an index (`step` / `defaultStep` / `onStepChange`). Titles come from `form.meta.steps`. Wrap **around** `FormRoot` so Enter advances. `FormStepFields` + `FormStepHeading` + `FormStepList` (`ToggleGroup`) + `FormStepNav` (Next validates the step; last step renders `children`, usually `FormActions`).
* `FormActions` — default save / submit / edit sit in a shadcn `ButtonGroup`. `save="auto"` hides Save draft when the session autosaves. `before` / `after` add controls without replacing Submit. `abandon` is opt-in. `saveProps` / `submitProps` merge onto the shadcn buttons (Base UI `render` included). `sticky` pins the bar. `FormRoot` / `FormActions` also take Base UI `render` to replace the host element.
* Built-in widgets (`TextField`, `BooleanField`, …) are usable on their own: `<EmailField binding={form.field("email")} />`. `ChoiceOption` and `StringField` are exported for custom widgets. Prefix / suffix / character count use shadcn `InputGroup`.

A widget receives `{ binding, className, mode }` — not a spread `FormFieldBinding`. Presentation stays on `meta`. Type-check the document with `satisfies FormDefinitionUi` so this package's keys autocomplete; extra `meta` keys are allowed. For `saveForm` rows, pass a Zod object to `dimahForm({ metaSchema })`.

```ts
import { defineForm } from "@dimah-form/server";
import type { FormDefinitionUi } from "@dimah-form/ui/types";

export const onboarding = defineForm({
  title: "Onboarding",
  meta: { layout: "steps", steps: { "1": "About you", "2": "Work" } },
  fields: [
    {
      id: "name",
      type: "text",
      required: true,
      meta: { placeholder: "Ada", step: 1 },
    },
    {
      id: "role",
      type: "select",
      meta: { widget: "radio", step: 2 },
      options: [{ value: "eng", label: "Engineer" }],
    },
  ],
} satisfies FormDefinitionUi);
```

| Key                         | Effect                                                                                    |
| --------------------------- | ----------------------------------------------------------------------------------------- |
| `multiline`                 | `text` → textarea                                                                         |
| `rows`                      | textarea rows                                                                             |
| `placeholder`               | input / select placeholder                                                                |
| `widget`                    | custom registry key, or built-in variant `radio` / `switch` / `chips` (not registry keys) |
| `section`                   | heading in `FormFields`                                                                   |
| `step`                      | page in `FormSteps`                                                                       |
| `width: "half"` / `"third"` | share a row                                                                               |
| `prefix` / `suffix`         | shadcn `InputGroup` addon on the control                                                  |
| `help`                      | persistent hint under the control                                                         |
| `autocomplete`              | native `autoComplete` (`email` defaults to `email`)                                       |
| `inputMode`                 | native `inputMode`                                                                        |
| `orientation`               | `FormFieldFrame` orientation                                                              |
| `option.meta.description`   | helper under a radio / checkbox option                                                    |
| `form.meta.layout`          | `auto` / `fill` / `steps` / `review`                                                      |
| `form.meta.steps`           | step key → heading                                                                        |
| `form.meta.submitLabel`     | submit button copy                                                                        |
| `unsetOnOff` (boolean)      | field key, not meta: off → `null` (consent)                                               |

RTL: logical CSS from day one (`text-start`, `ms-*`, `ps-*`). Colors: `*-dimah-form-*` (defaults to your shadcn theme). Chrome roots expose `data-slot="form-root"` / `form-actions` / … for CSS.

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Widgets" href="/docs/widgets" description="Bind FormFieldBinding yourself when you do not want the optional UI package." />

  <Card title="React Client" href="/docs/react" description="useFormResponse — the session this package wraps." />

  <Card title="Custom Fields" href="/docs/custom-fields" description="defineFieldType on the server, then a widget on the registry." />
</Cards>
