SinglePreview
Schema-driven single-record forms with grouped layouts, conditional fields, and wizard steps.
FormBuilder.Single renders one form from a Zod schema and formDefs. It owns the Cubitt field UI, form wiring, conditional visibility, validation display, and optional stepper controls.
Usage
import { z } from "zod";
import {
FormBuilder,
type FormDefs,
} from "@tilt-legal/cubitt-components/form-builder";const schema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "At least 8 characters"),
});
const formDefs = [
{
kind: "field",
name: "email",
label: "Email",
component: "email",
size: "full",
},
{
kind: "field",
name: "password",
label: "Password",
component: "text",
size: "full",
},
] as const satisfies FormDefs;<FormBuilder.Single
defaultValues={{ email: "", password: "" }}
formDefs={formDefs}
onSubmit={async (values) => {
await signIn(values);
}}
schema={schema}
submitLabel="Sign in"
/>Examples
Minimal Sign-In
<FormBuilder.Single
defaultValues={{ email: "", password: "", tags: [], phone: "" }}
formDefs={signInDefs}
onSubmit={handleSubmit}
schema={signInSchema}
submitLabel="Sign in"
/>Grouped Layout
<FormBuilder.Single
defaultValues={{
title: "",
code: "",
description: "",
practiceArea: "",
priority: "normal",
confidential: false,
}}
formDefs={groupedDefs}
onSubmit={handleSubmit}
schema={groupedSchema}
submitLabel="Save"
/>Conditional Logic
const formDefs = [
{
kind: "field",
name: "email",
component: "email",
showIf: (values) => values.mode === "advanced",
requiredIf: (values) => values.mode === "advanced",
},
];Multi-Step Wizard
const stepper = FormBuilder.Single.useStepper(() => null);
<FormBuilder.Single
defaultValues={wizardDefaults}
formDefs={wizardDefs}
onSubmit={handleSubmit}
schema={wizardSchema}
stepper={stepper.render}
validateOnBack
/>;Stepper Navigation
const stepper = FormBuilder.Single.useStepper(
(slot) => slot.defaultStepper
);
<Stepper
onValueChange={(index) => stepper.actions.goTo(index)}
value={stepper.state.index}
>
{steps.map((step, index) => (
<StepperItem
disabled={index > stepper.state.index}
key={step.id}
step={index}
>
<StepperTrigger>
<StepperIndicator />
<StepperHeading>
<StepperTitle>{step.title}</StepperTitle>
</StepperHeading>
</StepperTrigger>
</StepperItem>
))}
</Stepper>;External Submit
const formId = "member-form";
<FormBuilder.Single
defaultValues={{ name: "" }}
formDefs={formDefs}
id={formId}
onSubmit={handleSubmit}
schema={schema}
/>;
<Button form={formId} type="submit">
Submit
</Button>;API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
schema | z.ZodTypeAny | - | Zod schema used for validation and type inference |
formDefs | FormDefs<T> | - | Step, group, and field definitions |
onSubmit | (values: T) => FormSubmitResult | - | Submit handler. Return a ZodError to display server validation |
defaultValues | Partial<T> | {} | Initial TanStack Form values |
defaultStep | number | string | - | Initial step index or step id |
validateOnBack | boolean | false | Validate the current step before moving backward |
title | string | - | Optional heading rendered above the form |
description | string | - | Optional description rendered under the heading |
disabled | boolean | false | Disable fields, stepper actions, and the default submit button |
id | string | - | Form id for external submit buttons |
submitLabel | string | null | "Submit" | Built-in submit button label. Pass null to hide |
footer | ReactNode | - | Content rendered after fields before submit controls |
stepper | false | StepperRender<T> | - | Hide, replace, or extend the default stepper controls |
groupClassName | string | - | Layout class for group wrappers |
itemClassName | string | - | Layout class for field containers |
className | string | - | Layout class for the form element |
submitRef | Ref<() => void> | - | Imperative submit function |
formRef | Ref<AnyReactFormApi> | - | Underlying TanStack Form instance |
Stepper Hook
FormBuilder.Single.useStepper() returns a handle for external navigation.
| Property | Type | Description |
|---|---|---|
state.index | number | Current visible step index |
state.total | number | Total visible steps |
state.isFirst | boolean | Whether the first visible step is active |
state.isLast | boolean | Whether the last visible step is active |
state.disabled | boolean | Mirrors the form disabled state |
state.step | FormStep | null | Active step definition |
actions.next | () => Promise<boolean> | Validate and advance |
actions.previous | () => Promise<boolean> | Move backward |
actions.goTo | (target: number | string) => Promise<boolean> | Move to a step index or id |
actions.submit | () => Promise<boolean> | Validate current step and submit |
actions.reset | () => void | Reset to the first step |
render | (slot) => ReactNode | Pass to the stepper prop |