Form Component

API reference for the <Form /> component — props, the onSubmit caveat, and the FormRef imperative handle.

<Form /> Component

The main entry point of @saastro/forms. It takes a FormConfig, builds the Zod schema, wires up React Hook Form, renders the current step’s fields through your injected UI components, and runs the full submit pipeline.

import { Form } from '@saastro/forms';

<Form
  config={config}
  components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}
/>;

Props

<Form> accepts exactly five props (FormProps):

PropTypeDefaultDescription
configFormConfig— requiredThe full form configuration: fields, steps, layout, buttons, submit actions, plugins. See Types Reference
componentsComponentOverrides | GlobModulesundefinedUI components for the form — a component object or a raw import.meta.glob(..., { eager: true }) result (auto-detected)
onSubmit(values: Record<string, unknown>) => void | Promise<void>undefinedOnly fires on native form submission (e.g. pressing Enter) — not when the rendered submit button is clicked. See below
onError(error: Error) => voidundefinedNative-path only, like onSubmit — and submit-pipeline failures never reach it (they’re caught internally). See below
classNamestringundefinedCSS class for the <form> element. When omitted, the form falls back to w-full

config

The only required prop. Build it with FormBuilder or write the object by hand — the shape is documented in the Types Reference. Note that the identifier property is formId (not id), steps is a Record<string, Step> keyed by step id, and column count lives under layout.

Most behavior callbacks live on the config, not on the component: config.onSuccess, config.onError, config.onStepChange, config.successMessage, config.redirect, and so on.

components

Accepts either form:

// 1. Glob modules (Vite) — auto-discovers every component in the folder.
//    `eager: true` is required.
<Form config={config} components={import.meta.glob('@/components/ui/*.tsx', { eager: true })} />

// 2. Explicit component object — provide only what your form needs
<Form config={config} components={{ Input, Button, Field, FieldLabel, FieldDescription, FieldError, FormField, FormControl }} />

If you render <Form> with no components prop and no surrounding provider, the form is not rendered — you get a “Missing UI Components” error panel with setup instructions instead. A partial registry renders the form, and any individually missing components show a per-field fallback with install instructions.

Provider precedence: inside a legacy ComponentProvider, the provider’s registry wins and the components prop is effectively ignored. Use one mechanism or the other. See Component System for all three injection modes.

onSubmit and onError — read this before using them

The submit button that <Form> renders is deliberately type="button": clicking it goes through the library’s internal pipeline (confirmation UX → per-step validation → submit actions → config.onSuccess), bypassing native form submission entirely. The onSubmit prop is attached to the native <form onSubmit> handler, so:

  • onSubmit fires only when the form is submitted natively — for example, pressing Enter in a field — and only on the last step, after the internal submit pipeline finishes. Note it fires even if the pipeline failed: pipeline errors are caught internally (they set the error UI and call config.onError), so the native handler never sees them.
  • onError fires only on that same native path, and — because pipeline errors are swallowed internally — in practice only when your own onSubmit callback throws.
  • Clicking the rendered submit button never calls either prop.

For callbacks that fire on every successful or failed submission, use the config instead:

const config = FormBuilder.create('contact')
  // ...fields and steps...
  .onSuccess((values) => console.log('submitted', values))
  .onError((error, values) => console.error(error))
  .build();

See Submitting Forms for the full pipeline.

className

Applied to the underlying <form> element. Defaults to w-full when omitted. For styling fields and the grid, see Styling and Layout System.


Imperative Handle: FormRef

<Form> is a forwardRef component. Attach a ref to control the form from outside — prefill values, reset it, read the current values, or trigger validation. The handle exposes exactly four methods, all passed through from the underlying React Hook Form instance:

import type { UseFormReturn } from 'react-hook-form';

interface FormRef {
  setValue: UseFormReturn['setValue']; // setValue(name, value, options?)
  reset: UseFormReturn['reset']; // reset(values?, options?)
  getValues: UseFormReturn['getValues']; // getValues() / getValues(name)
  trigger: UseFormReturn['trigger']; // trigger(name?) => Promise<boolean>
}
MethodSignatureDescription
setValue(name, value, options?) => voidSet a field’s value programmatically
reset(values?, options?) => voidReset the form to its defaults, or to the values you pass
getValues(name?) => valuesRead current values — all of them, or a single field by name
trigger(name?) => Promise<boolean>Run validation for one field, several, or the whole form; resolves true when valid

Example

import { useRef } from 'react';
import { Form, FormBuilder, type FormRef } from '@saastro/forms';

const config = FormBuilder.create('newsletter')
  .addField('email', (f) => f.type('email').label('Email').required().email())
  .addStep('main', ['email'])
  .build();

export function Newsletter() {
  const formRef = useRef<FormRef>(null);

  return (
    <div>
      <button type="button" onClick={() => formRef.current?.setValue('email', '[email protected]')}>
        Prefill
      </button>
      <button type="button" onClick={() => formRef.current?.reset()}>
        Clear
      </button>
      <button type="button" onClick={() => console.log(formRef.current?.getValues())}>
        Log values
      </button>
      <button
        type="button"
        onClick={async () => {
          const valid = await formRef.current?.trigger();
          console.log(valid ? 'Valid' : 'Has errors');
        }}
      >
        Validate
      </button>

      <Form
        ref={formRef}
        config={config}
        components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}
      />
    </div>
  );
}

trigger() validates and shows error messages, but it does not submit. There is no imperative submit method — submission goes through the rendered submit button. (To fire individual submit actions imperatively — outside the full pipeline — see manual submit action triggers.)


Success and error UI

After a successful submission, the entire form is replaced by a success panel. The default message is '✅ Thank you!' — one of the library’s built-in English defaults (along with button labels like Submit). Override it per form with config.successMessage, per locale via Internationalization, or globally with setDefaultMessages. When a submission fails, an error panel renders below the form with config.errorMessage (or its default).

String success/error messages are rendered as HTML, so successMessage: 'Done! <a href="/next">Continue</a>' works. The HTML is sanitized (same for HTML field labels): <script>/<iframe>/<svg>-class elements, on* handlers, and javascript: URLs are stripped — formatting tags and safe links pass through. The html field type is the unsanitized escape hatch for trusted markup.

Hardened apps can replace the built-in sanitizer globally:

import DOMPurify from 'dompurify';
import { setHtmlSanitizer } from '@saastro/forms';

setHtmlSanitizer((html) => DOMPurify.sanitize(html));

  • Quickstart — Build and render your first form
  • Component System — All three component-injection modes and auto-discovery details
  • Submitting Forms — What happens when the user clicks submit
  • Types ReferenceFormConfig and every other exported type
  • HubForm — A wrapper around <Form> that fetches its config from the hosted submit service