Form Definitions

Reference for the shared formDefs structure used by FormBuilder.Single and FormBuilder.Bulk.

formDefs is a nested array of step, group, and field nodes that describes the structure, order, and behaviour of fields. Both FormBuilder.Single and FormBuilder.Bulk consume the same definition array, so you only maintain one source of truth.

Usage

import type { FormDefs } from "@tilt-legal/cubitt-components/form-builder";

const formDefs = [
  // Fields, groups, and steps...
] as const satisfies FormDefs<MyFormData>;

Always use as const satisfies FormDefs<T> for full type safety with your schema. The same FormDefs type works for both Single and Bulk forms.

How formDefs Are Used

  • FormBuilder.Single renders a dynamic form UI, wiring each field to TanStack Form and Zod validation
  • FormBuilder.Bulk generates spreadsheet columns, CSV templates, and value coercion logic

Changes to labels, options, or field metadata are reflected in both experiences.

Step Nodes

Steps create multi-step wizard flows. They're only used by FormBuilder.Single; FormBuilder.Bulk ignores them.

const step = {
  kind: "step",
  id: "contact",
  title: "Contact Details",
  description: "Primary contact information",
  nextLabel: "Continue",
  previousLabel: "Go Back",
  showIf: (values) => values.enableWizard,
  onEnter: (values) => console.log("Entered step"),
  onExit: (values) => console.log("Exited step"),
  children: [
    // Groups and fields...
  ],
};

Step Properties

PropertyTypeDescription
kind"step"Always "step"
idstringStable identifier for defaultStep and stepper.actions.goTo
titlestringRendered in default navigation and exposed on stepper state
descriptionstringOptional helper copy for the step
nextLabelstringOverrides default "Next"/"Submit" button text
previousLabelstringOverrides default "Back" button text
showIf(values) => booleanWhen false, the step is skipped entirely
onEnter(values) => voidCallback when the step becomes active
onExit(values) => voidCallback when the step is left
childrenarrayGroups and fields rendered when active

When you mix loose groups/fields with step nodes, the loose nodes are wrapped into an implicit step automatically.

Group Nodes

Groups visually organise related fields with optional titles:

const group = {
  kind: "group",
  name: "organisation",
  title: "Organisation",
  description: "Company details for invoicing",
  gap: 24,
  minWidth: 280,
  children: [
    {
      kind: "field",
      name: "companyName",
      label: "Company",
      component: "text",
      size: "full",
    },
    {
      kind: "field",
      name: "taxId",
      label: "Tax ID",
      component: "text",
      size: "half",
    },
  ],
};

Group Properties

PropertyTypeDescription
kind"group"Always "group"
namestringOptional schema path used to bind object-level validation errors to the fieldset
titlestringRendered as the group title via FormFieldSet for accessibility
descriptionstringHelper copy shown below the title
gapnumberGap in pixels between children (default: 22)
minWidthnumberMinimum width before items wrap (default: 180)
childrenarrayNested groups and fields

Add name when the schema can raise an error for the object as a whole, such as a checkbox collection at practiceAreas. Cubitt will render that message once under the full fieldset instead of forcing you to pin the error to the first child field.

Field Nodes

Fields define individual form inputs:

const field = {
  kind: "field",
  name: "email",
  label: "Email Address",
  description: "We'll send confirmation here",
  tooltip: "Must be a valid work email",
  component: "email",
  size: "full",
  showIf: (values) => values.notificationType === "email",
  requiredIf: (values) => values.notificationType === "email",
  disabledIf: (values) => values.locked,
  clearWhenHidden: true,
};

Common Field Properties

PropertyTypeDescription
kind"field"Always "field"
namestringDot/bracket path matching schema (e.g., "contact.email")
componentFieldComponentDetermines which field type renders (see below)
labelstringField label (defaults to humanised name)
descriptionstringInline helper text below the label
tooltipstringTooltip content shown next to the label
size"full" | "half" | "third"Responsive width hint
breakBeforebooleanForce field onto a new row
clearWhenHiddenbooleanClear value when hidden (default: true)

Conditional Predicates

PropertyTypeDescription
showIf(values) => booleanShow/hide the field
disabledIf(values) => booleanDisable the field
readOnlyIf(values) => booleanMake field read-only
requiredIf(values) => booleanAdd required indicator

Component Types

Available Components

ComponentDescription
"text"Standard text input
"textarea"Multi-line text
"email"Email input with validation
"number"Numeric input
"phone"Phone number with country code
"date"Date picker
"select"Dropdown select
"radio"Radio button group
"checkbox"Checkbox input
"switch"Toggle switch
"slider"Range slider
"tags"Tag/chip input
"autocomplete"Autocomplete with search

Select and Radio Options

For select and radio components, provide an options array:

{
  kind: "field",
  name: "role",
  label: "Role",
  component: "select",
  options: [
    { value: "admin", label: "Administrator" },
    { value: "member", label: "Member" },
    { value: "guest", label: "Guest", disabled: true },
  ],
  size: "full",
}
PropertyTypeDescription
optionsOption[]Required for select and radio

Option type:

type Option = {
  id?: string | number;
  value: string;
  label: string;
  disabled?: boolean;
};

Radio-Specific Properties

PropertyTypeDescription
verticalbooleanDisplay options vertically

Text/Email/Number-Specific Properties

PropertyTypeDescription
inputPropsInputHTMLAttributesHTML attributes passed to the input element (e.g., autoComplete, inputMode)

Useful for browser autofill hints and password manager integration:

{ kind: "field", name: "email", component: "email", inputProps: { autoComplete: "email" } }
{ kind: "field", name: "password", component: "text", inputProps: { autoComplete: "current-password" } }
{ kind: "field", name: "apiKey", component: "text", inputProps: { "data-1p-ignore": true, autoComplete: "off" } }

Phone-Specific Properties

PropertyTypeDescription
defaultCountrystringDefault country code (e.g., "US")

Tags-Specific Properties

PropertyTypeDescription
enableCommaDelimiterbooleanCreate tags on comma (default: true)

Number/Slider-Specific Properties

PropertyTypeDescription
minnumberMinimum value
maxnumberMaximum value
stepnumberStep increment
valueLabelboolean | ((props) => ReactNode)Show value label or custom render

Autocomplete-Specific Properties

PropertyTypeDescription
autocompleteItemsunknown[]Items to display in the list
autocompleteItemToString(item) => stringConvert item to display string
autocompleteItemToKey(item) => stringUnique key for each item
autocompleteEmptyMessagestringMessage when no results (default: "No results found.")

Complete Example

import { z } from "zod";
import type { FormDefs } from "@tilt-legal/cubitt-components/form-builder";

const schema = z.object({
  firstName: z.string().min(1, "Required"),
  lastName: z.string().min(1, "Required"),
  email: z.string().email(),
  role: z.enum(["admin", "member"]),
  department: z.string().optional(),
  notifications: z.boolean().default(true),
});

type FormData = z.infer<typeof schema>;

const formDefs = [
  {
    kind: "group",
    title: "Personal Info",
    children: [
      {
        kind: "field",
        name: "firstName",
        label: "First name",
        component: "text",
        size: "half",
      },
      {
        kind: "field",
        name: "lastName",
        label: "Last name",
        component: "text",
        size: "half",
      },
      {
        kind: "field",
        name: "email",
        label: "Email",
        component: "email",
        size: "full",
      },
    ],
  },
  {
    kind: "group",
    title: "Access",
    children: [
      {
        kind: "field",
        name: "role",
        label: "Role",
        component: "select",
        options: [
          { value: "admin", label: "Admin" },
          { value: "member", label: "Member" },
        ],
        size: "half",
      },
      {
        kind: "field",
        name: "department",
        label: "Department",
        component: "text",
        size: "half",
        showIf: (values) => values.role === "admin",
      },
      {
        kind: "field",
        name: "notifications",
        label: "Email notifications",
        component: "switch",
        size: "full",
      },
    ],
  },
] as const satisfies FormDefs<FormData>;

On this page