---
title: Form
description: The whole form from a schema: fields, submit, inline errors and the success state, with zero markup.
preview: form
---
# Form

`Form` takes a schema and a client and renders a finished form. With no children it renders every page itself; with children you compose the layout from the other components.

## Preview
<!-- preview: form -->

## Usage
```tsx
import { Form } from 'kibbleforms';
import { kibble } from '@/kibble/client';
import { waitlist } from '@/kibble/forms/waitlist';

export function Waitlist() {
  return <Form schema={waitlist} client={kibble} />;
}
```

One page renders the fields and a [Submit](/docs/components/submit.md) button. Several pages render [MultiStep](/docs/components/multi-step.md). After a successful submit the form is replaced by [Success](/docs/components/success.md), unless `settings.success` is a redirect or schedule, in which case the browser navigates.

## Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `schema` | `FormSchema` | required | The schema from `defineForm`. |
| `client` | `KibbleClient` | nearest `KibbleProvider` | From `createClient` (`kibble/client.ts`). Throws a descriptive error when neither is present. |
| `onSuccess` | `(result: SubmissionResponse) => void` | — | Called with `{ id, status, success?, flags? }`. |
| `onError` | `(error: KibbleError) => void` | — | Called with `{ code, message, hint?, fields?, status }`. |
| `initialValues` | `Record<string, unknown>` | — | Programmatic prefill, keyed by field key. Wins over `prefill.query` and `defaultValue`. |
| `children` | `ReactNode` | auto-render | Replaces the automatic layout. Use `Field`, `Submit`, `MultiStep` etc. inside. |
| `showSuccess` | `boolean` | `true` | Swap the form for `Success` after submit. |
| `requiredIndicator` | `ReactNode \| null` | nothing | What labels append to required fields. See [Label](/docs/components/label-help-error.md). |
| `optionalIndicator` | `ReactNode \| null` | muted "Optional" | What labels append to optional fields; `null` for nothing. |
| `theme` | `KibbleTheme` | from `KibbleProvider` | `primary`, `primaryForeground`, `background`, `foreground`, `muted`, `mutedForeground`, `border`, `ring`, `error`, `inputBackground`, `radius` (`'none' \| 'sm' \| 'md' \| 'lg' \| 'full'` or a CSS length), `font`, `colorScheme` (`'light' \| 'dark' \| 'auto'`). Inline `--kb-*` variables on the root; merged over the provider's per key. See [Theming](/docs/theming.md). |
| `size` | `'sm' \| 'md' \| 'lg'` | provider's, else `'md'` | Token bundle for control height, text size and spacing (`data-kb-size`). |
| `layout` | `{ columns?: 1 \| 2 \| 3; full?: string[] }` | one column | Grid for the auto-rendered fields and `Page`. Long text, choices, consent and `full` keys span all columns; one column below 640px. |
| `classNames` | `Partial<Record<Slot, string>>` | — | Classes per part: `root`, `form`, `page`, `pageHeader`, `field`, `label`, `help`, `input`, `option`, `segment`, `rating`, `consent`, `error`, `submit`, `button`, `progress`, `success`. Merged after the provider's and before each component's own `className`. |
| `className` | `string` | `flex flex-col gap-kb` | Merged onto the inner `<form>` (after `classNames.form`). |
| `rootClassName` | `string` | `font-kb text-kb-fg` | Merged onto the outer `[data-kibble]` wrapper (after `classNames.root`). |
| …`<form>` attributes | | | Everything else (`id`, `aria-label`, …) is spread on the `<form>`. `aria-label` defaults to `schema.name` unless you pass `aria-label` or `aria-labelledby`. |

Rendered structure:

```html
<div data-kibble data-kb="form" data-slug="waitlist" data-status="idle|validating|submitting|success|error"
     data-kb-size="md" [data-kb-color-scheme="dark"] [style="--kb-primary: …"]>
  <form novalidate aria-label="Waitlist">
    <div data-kb="honeypot">…</div>            <!-- visually hidden, no CSS needed -->
    …fields…
    <div data-kb="turnstile"></div>             <!-- only when a Turnstile site key is set -->
    <div role="alert" data-kb="form-error" data-code="…"><p>message</p><p>hint</p></div>  <!-- only on a form-level error -->
  </form>
</div>
```

Form-level errors (network, `rate_limited`, `project_unclaimed`, `agents_blocked`, …) render the server's `message` and `hint` in `data-kb="form-error"`. Field-level server errors (`validation_failed` with `fields`) go to the matching [FieldError](/docs/components/label-help-error.md) instead and focus moves to the first one.

## Variants and states

Composed layout: children replace the automatic rendering.

<!-- preview: form-composed -->

```tsx
import { Form, Field, Submit } from 'kibbleforms';

<Form schema={contact} client={kibble} className="gap-5">
  <div className="grid grid-cols-2 gap-4">
    <Field name="first_name" />
    <Field name="last_name" />
  </div>
  <Field name="email" />
  <Field name="message" />
  <Submit fullWidth={false} className="self-end" />
</Form>
```

Rethemed with the `theme` prop, same markup:

<!-- preview: form-themed -->

```tsx
<Form schema={waitlist} client={kibble} theme={{ primary: '#2563eb', ring: '#2563eb', radius: 'full', border: '#bfdbfe', muted: '#eff6ff', mutedForeground: '#1d4ed8' }} />
```

Dark, following the site: under a `.dark` (or `[data-theme="dark"]`) ancestor the built-in dark values apply. `theme={{ colorScheme: 'dark' }}` forces them on one form.

<!-- preview: form-dark -->

```tsx
<div className="dark">
  <Form schema={waitlist} client={kibble} />
</div>
```

Two columns and a larger size:

```tsx
<Form schema={demo} client={kibble} size="lg" layout={{ columns: 2, full: ['company_url'] }} />
```

Status is exposed as `data-status` on the wrapper, so `[data-kibble][data-status="submitting"]` can style the whole form while a request is in flight.

## Customization
- Variables the wrapper reads: `--kb-font`, `--kb-fg`, `--kb-text`, `--kb-space` (the gap between fields via `gap-kb`), `--kb-color-scheme`. Every child reads its own tokens; `theme`, `size` and `colorScheme` only change variables. See [Styling](/docs/styling.md) and [Theming](/docs/theming.md).
- `className` is merged with tailwind-merge, so `className="gap-2"` replaces the default `gap-kb`; `className="grid grid-cols-2"` replaces `flex flex-col`.
- CSS hooks: `[data-kibble]` (scope), `[data-kb="form"]`, `[data-kb="form-error"]`, `[data-status="…"]`, `[data-slug="…"]`, `[data-kb-size="…"]`, `[data-kb-color-scheme="…"]`.
- To change the structure itself, compose children (`Field`, `Submit`, your own JSX) or build on the [headless](/docs/components/headless.md) hooks.

## Accessibility
- Renders a real `<form>` with `novalidate`: validation messages are Kibble's (inline, below each control), not the browser's bubbles. `aria-label` is the form name unless you label it yourself.
- Enter in a single-line input submits a single-page form, or advances to the next page in a multi-step form.
- On a failed submit, focus moves to the first invalid control (jumping to its page in a multi-step form). On a page change, focus moves to the page heading, or the first field when the page has no title.
- Form-level errors use `role="alert"`.
- The honeypot is `aria-hidden`, `tabindex="-1"` and off-screen; assistive technology never meets it.

## Related
[Field](/docs/components/field.md), [MultiStep](/docs/components/multi-step.md), [Success](/docs/components/success.md), [KibbleProvider](/docs/components/provider.md), [Headless](/docs/components/headless.md), [Schema](/docs/schema.md), [Styling](/docs/styling.md), [Theming](/docs/theming.md)
