Documentation
Documentation
Introduction

Getting Started

Getting StartedInstallationQuick StartProject Structure

Architecture

Architecture OverviewTech StacktRPC MiddlewareDesign Principles

Patterns

Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

DatabaseSchema DefinitionDatabase OperationsMigrationsCaching

API

tRPCProceduresRouterstRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

StorageConfigurationUsageBuckets

Configuration

ConfigurationEnvironment VariablesFeature Flags

Templates

Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate tRPC RouterAdd Translations

Development

DevelopmentCommandsAI AgentsBest Practices

Forms

Form components and validation

Forms use React Hook Form with Zod validation for type-safe, performant form handling.

Basic Form Structure

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { z } from "zod";

const schema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
});

type FormData = z.infer<typeof schema>;

function MyForm() {
  const form = useForm<FormData>({
    resolver: zodResolver(schema),
    defaultValues: {
      email: "",
      name: "",
    },
  });

  const onSubmit = async (data: FormData) => {
    // Handle form submission
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input {...field} type="email" />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <SubmitButton i18nButtonKey="save" />
      </form>
    </Form>
  );
}

FormField Pattern

Use the FormField component to integrate inputs with react-hook-form:

<FormField
  control={form.control}
  name="fieldName"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Label</FormLabel>
      <FormControl>
        <Input {...field} />
      </FormControl>
      <FormDescription>Optional helper text</FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

FormField Props

PropDescription
controlForm control from form.control
nameField name (must match schema)
renderRender function with field and error

Submit Button

Use the SubmitButton component for automatic integration with form state:

import { SubmitButton } from "@/forms/submit-button";

<SubmitButton i18nButtonKey="save" />

Props

PropTypeDefaultDescription
i18nButtonKeystring—Translation key for button text
skipDirtyCheckbooleanfalseAllow submit even when form is pristine

By default, SubmitButton is disabled when the form is pristine (no changes). Use skipDirtyCheck={true} for create forms or when re-submitting should be allowed.

Form Validation

Validation errors are automatically translated using Zod error messages.

Custom Error Messages

const schema = z.object({
  password: z.string().min(8, "Password must be at least 8 characters"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords must match",
  path: ["confirmPassword"],
});

File Upload

Use FileUpload for file inputs with drag-and-drop:

import { FileUpload } from "@/forms/file-upload";

<FileUpload
  accept="image/*"
  maxSize={5 * 1024 * 1024} // 5MB
  onUpload={handleUpload}
/>

Specialized Pickers

EnumPicker

Select from predefined enum values with custom rendering:

import EnumPicker from "@/forms/enum-picker";

<EnumPicker
  items={MyStatus}
  renderItem={(item) => ({
    title: t(`status.${item}`),
    description: t(`status.${item}Description`),
    color: statusColors[item],
  })}
  value={form.watch("status")}
  onChange={(val) => form.setValue("status", val)}
  canClear
  name="status"
/>

NumberPicker

Number input with increment/decrement buttons:

import { NumberPicker } from "@/forms/number-picker";

<NumberPicker
  value={quantity}
  onChange={setQuantity}
  min={1}
  max={100}
  step={1}
  name="quantity"
/>

AsyncPicker

Async data picker with search and tRPC integration:

import AsyncPicker from "@/forms/async-picker";

<AsyncPicker<User>
  onSearch={async (query) => {
    const result = await utils.users.search.fetch({ query });
    return result.payload ?? [];
  }}
  onFetchSelected={async (ids) => {
    const result = await utils.users.getByIds.fetch({ ids });
    return result.payload ?? [];
  }}
  getItemId={(user) => user.id}
  renderItem={(user) => (
    <div className="flex items-center gap-2">
      <Avatar src={user.image} size="sm" />
      <span>{user.name}</span>
    </div>
  )}
  value={selectedUserId}
  onChange={(id, user) => setSelectedUserId(id)}
  allowClear
/>

MultiSelector

Multi-select with tag-style display:

import {
  MultiSelector,
  MultiSelectorTrigger,
  MultiSelectorInput,
  MultiSelectorContent,
  MultiSelectorList,
  MultiSelectorItem,
} from "@/forms/multi-select";

<MultiSelector
  values={tags}
  onValuesChange={setTags}
  allowInsert
>
  <MultiSelectorTrigger>
    <MultiSelectorInput placeholder="Add tags..." />
  </MultiSelectorTrigger>
  <MultiSelectorContent>
    <MultiSelectorList>
      {suggestedTags.map((tag) => (
        <MultiSelectorItem key={tag} value={tag}>
          {tag}
        </MultiSelectorItem>
      ))}
    </MultiSelectorList>
  </MultiSelectorContent>
</MultiSelector>

Best Practices

Define validation with Zod for type-safety and automatic error messages

Ensures proper integration with react-hook-form

Override default Zod messages when needed for better UX

Automatically handles loading and disabled states

On this page

Basic Form Structure
FormField Pattern
FormField Props
Submit Button
Props
Form Validation
Custom Error Messages
File Upload
Specialized Pickers
EnumPicker
NumberPicker
AsyncPicker
MultiSelector
Best Practices