Quickstart

Build and render your first working form in five minutes.

Quickstart

Build a working contact form in five minutes: install the package, auto-discover your shadcn/ui components, define three fields with FormBuilder, render <Form>, and read the submitted values.

Prerequisites: a React project on Vite with shadcn/ui initialized (a components.json and the @/ path alias). Not on Vite? Everything still works — you just pass components explicitly instead of using glob auto-discovery (see Installation and Components).

1. Install

# Install the package
npm install @saastro/forms

# Install required shadcn components + missing peer dependencies
npx @saastro/forms install-deps

install-deps reads your components.json, adds any required shadcn components, and installs missing peer dependencies (zod, react-hook-form, react-day-picker, date-fns). See Installation for manual setup and the CLI reference for all flags.

2. Define and render the form

Create src/components/ContactForm.tsx:

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

// Auto-discover every shadcn component in src/components/ui (Vite only).
// `eager: true` is required — lazy globs are not supported.
const uiComponents = import.meta.glob('@/components/ui/*.tsx', { eager: true });

const config = FormBuilder.create('contact')
  .addField('name', (f) =>
    f.type('text').label('Name').placeholder('Jane Doe').required('Name is required'),
  )
  .addField('email', (f) =>
    f.type('email').label('Email').required().email('Please enter a valid email'),
  )
  .addField('message', (f) => f.type('textarea').label('Message').rows(4).required())
  .addStep('main', ['name', 'email', 'message'])
  .buttons({ submit: { type: 'submit', label: 'Send' } })
  .successMessage('Thanks! We got your message.')
  .onSuccess((values) => {
    // Fires after a successful submit
    console.log(values); // { name: '...', email: '...', message: '...' }
  })
  .build();

export function ContactForm() {
  return <Form config={config} components={uiComponents} />;
}

Then render it anywhere in your app:

import { ContactForm } from './components/ContactForm';

export default function App() {
  return (
    <main className="mx-auto max-w-md p-8">
      <ContactForm />
    </main>
  );
}

Run your dev server — you have a validated, type-safe form.

A few things worth knowing about what you just wrote:

  • Every form needs at least one step. Even single-page forms declare one (.addStep('main', [...])); build() throws without it. Multi-page wizards just add more steps — see Multi-Step Forms.
  • Validation is declarative. .required() and .email() build a JSON-serializable rules object that is compiled to Zod at runtime. You can also pass a Zod schema directly with .schema() — see Validation.
  • import.meta.glob is a Vite feature. It does not exist in Next.js or webpack. On other bundlers, pass components explicitly: components={{ Input, Button, ... }} — see Components.
  • Default UI strings are in English. Out of the box the submit button says Submit and the success panel says ✅ Thank you! — the example sets label: 'Send' and a custom successMessage only to show how to override them. Building in another language? Flip every default globally (e.g. Spanish: setDefaultMessages(es)) or translate per locale — see Internationalization.

3. Read the submitted values

Click Send. The form validates, runs the submit pipeline, replaces itself with the success message, and calls your .onSuccess() callback with the final values — a Record<string, unknown> keyed by field name, after any field transforms have been applied. With no submit destination configured (like this quickstart), nothing is sent anywhere — open the console to see the values.

Use .onSuccess(), not the <Form onSubmit> prop. The rendered submit button intentionally bypasses native form submission (so multi-step navigation and confirmation flows can intercept it), which means the onSubmit prop only fires on native submission — for example, pressing Enter. .onSuccess() fires on every successful submit, whichever way it was triggered.

4. Send the data somewhere

To actually deliver the submission, add a submit action to the builder chain before .build():

  .submitAction(
    'save',
    {
      type: 'http',
      name: 'Save lead',
      endpoint: { url: 'https://api.example.com/leads', method: 'POST' },
      body: { format: 'json' },
    },
    'onSubmit',
  )

Submit actions support HTTP requests, webhooks, email providers, and custom handlers, with field mapping, conditions, and retries — see Submit. Don’t have a backend? The hosted submit service handles storage, spam protection, and file uploads for you — see HubForm.

Next steps

  • Installation — manual setup, non-Vite projects, the CLI.
  • FormBuilder — the full fluent API, JSON configs, and serialization.
  • Validation — declarative rules, Zod schemas, custom validators.
  • Multi-Step Forms — wizards with conditional step routing.
  • Field Types — interactive examples of all 34 field types.