# Custom Fields (https://form.dimah.dev/docs/custom-fields)



Custom field types are **server validators**, not React components. You define how the answer is checked and typed. You render whatever control you want.

***

## 1. Defining a Custom Field Type [#1-defining-a-custom-field-type]

Use `defineFieldType()` from `@dimah-form/core` to declare a new field validator:

```ts title="lib/field-types/rating.ts"
import { defineFieldType } from "@dimah-form/core";
import * as z from "zod";

export const ratingFieldType = defineFieldType({
  type: "rating",

  // 1. Zod schema for validating the field's configuration in defineForm()
  fieldSchema: z.looseObject({
    type: z.literal("rating"),
    min: z.number().int().min(1).default(1),
    max: z.number().int().max(10).default(5),
  }),

  // 2. Server validator for respondent answer values
  validate: (value, field) => {
    if (typeof value !== "number" || !Number.isInteger(value)) {
      return {
        message: "Rating must be an integer",
        code: "EXPECTED_INTEGER",
      };
    }

    const min = field.min ?? 1;
    const max = field.max ?? 5;

    if (value < min) {
      return {
        message: `Rating must be at least ${min}`,
        code: "TOO_SMALL",
        params: { min },
      };
    }

    if (value > max) {
      return {
        message: `Rating cannot exceed ${max}`,
        code: "TOO_LARGE",
        params: { max },
      };
    }

    return undefined; // Valid!
  },

  // 3. Helper to determine if the field is empty for `required` checks
  isEmpty: (value) => value == null,

  // 4. Type token for TypeScript compile-time answer inference
  $Infer: 0 as number,
});
```

Export your custom field types from a central module:

```ts title="lib/field-types/index.ts"
import { ratingFieldType } from "./rating";

export const customFieldTypes = [ratingFieldType] as const;
```

***

## 2. Registering on Server and Client [#2-registering-on-server-and-client]

Register your custom field types in both `dimahForm()` and `createFormClient()`:

### Server Registration [#server-registration]

```ts title="lib/form.ts"
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { customFieldTypes } from "@/lib/field-types";
import { forms } from "@/lib/forms";

export const form = dimahForm({
  database: memoryAdapter(),
  fieldTypes: customFieldTypes,
  forms,
});

export type Form = typeof form;
```

### Client Registration [#client-registration]

```ts title="lib/form-client.ts"
"use client";

import { createFormClient } from "@dimah-form/react";
import { customFieldTypes } from "@/lib/field-types";
import type { Form } from "@/lib/form";

export const formClient = createFormClient<Form>({
  basePath: "/api/form",
  fieldTypes: customFieldTypes,
});

export const { useFormResponse } = formClient;
```

***

## 3. Using Custom Fields in Forms [#3-using-custom-fields-in-forms]

Define questionnaires with your new field type:

```ts title="lib/forms/review.ts"
import { defineForm } from "@dimah-form/server";

export const reviewForm = defineForm({
  title: "Product Review",
  slug: "review",
  fields: [
    {
      id: "score",
      type: "rating",
      label: "Star Rating (1-5)",
      required: true,
      min: 1,
      max: 5,
    },
    {
      id: "comments",
      type: "text",
      label: "Review Comments",
    },
  ],
});
```

TypeScript automatically infers `Form["$Infer"]["answers"]["review"]["score"]` as `number`.

***

## 4. Rendering Custom Widgets in React [#4-rendering-custom-widgets-in-react]

Add a branch to your `FormFieldControl` dispatcher to render an interactive star rating control:

```tsx title="components/form-field-control.tsx"
import { type FormFieldBinding } from "@dimah-form/react";

export function FormFieldControl(binding: FormFieldBinding) {
  const { field, value, error, disabled, required, onChange } = binding;
  if (!field) return null;

  // Custom Rating Widget
  if (field.type === "rating") {
    const currentRating = typeof value === "number" ? value : 0;
    const max = typeof field.max === "number" ? field.max : 5;

    return (
      <div className="space-y-1.5">
        <label className="block text-sm font-semibold">
          {field.label}
          {required && <span className="ml-1 text-red-500">*</span>}
        </label>
        <div className="flex gap-1">
          {Array.from({ length: max }, (_, i) => i + 1).map((star) => (
            <button
              key={star}
              type="button"
              disabled={disabled}
              onClick={() => onChange(star)}
              className={`text-2xl transition-colors ${
                star <= currentRating ? "text-yellow-400" : "text-gray-300"
              }`}
            >
              ★
            </button>
          ))}
        </div>
        {error && <p className="text-xs text-red-500">{error}</p>}
      </div>
    );
  }

  // Other field types...
  return null;
}
```

***

## Next Steps [#next-steps]

<Cards>
  <Card title="Field Types" href="/docs/field-types" description="Explore all 7 built-in field types and showWhen conditional rules." />

  <Card title="Widgets" href="/docs/widgets" description="Discover patterns for rendering complex, interactive inputs." />

  <Card title="Plugins" href="/docs/plugins" description="Bundle custom field types and custom endpoints into reusable plugins." />
</Cards>
