Form
The whole form from a schema: fields, submit, inline errors and the success state, with zero markup.
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
Usage
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 button. Several pages render MultiStep. After a successful submit the form is replaced by Success, 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. |
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. |
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:
<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 instead and focus moves to the first one.
Variants and states
Composed layout: children replace the automatic rendering.
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:
<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.
<div className="dark">
<Form schema={waitlist} client={kibble} />
</div>
Two columns and a larger size:
<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 viagap-kb),--kb-color-scheme. Every child reads its own tokens;theme,sizeandcolorSchemeonly change variables. See Styling and Theming. classNameis merged with tailwind-merge, soclassName="gap-2"replaces the defaultgap-kb;className="grid grid-cols-2"replacesflex 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 hooks.
Accessibility
- Renders a real
<form>withnovalidate: validation messages are Kibble's (inline, below each control), not the browser's bubbles.aria-labelis 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, MultiStep, Success, KibbleProvider, Headless, Schema, Styling, Theming