AutoForm
React

Customization

The customization of the components is done by providing a `fieldConfig` to your schema fields. This allows you to customize the rendering of the field, add additional props, and more.

Import fieldConfig from your schema package @acp-autoform/zod, @acp-autoform/yup, @acp-autoform/joi

In the examples below, we use Zod.

import * as z from "zod";
import { fieldConfig } from "@acp-autoform/zod";
import { FieldTypes } from "@acp-autoform/mui"; // use your UI library

const schema = z.object({
  username: z.string().check(
    fieldConfig({
      label: "Username",
      description: "Choose a unique username.",
      inputProps: {
        placeholder: "Enter your username",
      },
    }),
  ),

  password: z.string().check(
    fieldConfig({
      inputProps: {
        type: "password",
        placeholder: "Enter your password",
      },
    }),
  ),
});

Input props

You can use the inputProps property to pass props to the input component.

const schema = z.object({
  username: z.string().check(
    fieldConfig({
      inputProps: {
        type: "text",
        placeholder: "Username",
      },
    }),
  ),
});
// This will be rendered as:
<input type="text" placeholder="Username" /* ... */ />;

Description

You can use the description property to add a description below the field. You can use JSX in the description.

const schema = z.object({
  username: z.string().check(
    fieldConfig({
      description:
        "Enter a unique username. This will be shown to other users.",
    }),
  ),
});

Order

If you want to change the order of fields, use the order config. You can pass an arbitrary number where smaller numbers will be displayed first. All fields without a defined order use "0" so they appear in the same order they are defined in

const schema = z.object({
  termsOfService: z.boolean().check(
    fieldConfig({
      order: 1, // This will be displayed after other fields with order 0
    }),
  ),

  username: z.string().check(
    fieldConfig({
      order: -1, // This will be displayed first
    }),
  ),

  email: z.string().check(
    fieldConfig({
      // Without specifying an order, this will have order 0
    }),
  ),
});

Custom fields

You can customize fields in two ways: overriding existing UI components or adding new form components.

AutoForm renders every generated field inside the FieldWrapper UI component. The wrapper renders the resolved label, validation error and the field component as children. Because of this, normal form components should render only the actual input control.

Overriding default UI components

You can override the default UI components with custom components. This allows you to customize the look and feel, either globally or for specific fields.

Example: Creating custom FieldWrapper

The FieldWrapper is responsible for rendering the field label and error, so when you use a custom wrapper, you need to handle these yourself. You can take a look at the FieldWrapperProps type to see what props are passed.

// CustomFieldWrapper.tsx
import { FieldWrapperProps } from "@acp-autoform/react"; // adjust import based on your library

export function CustomFieldWrapper({
  children,
  label,
  error,
}: FieldWrapperProps) {
  return (
    <div>
      <label>{label}</label>
      {children}
      {error}
      <p className="text-muted-foreground text-sm">Custom wrapper element</p>
    </div>
  );
}

// App.tsx
import { CustomFieldWrapper } from "./CustomFieldWrapper";

// 1. Globally override the field wrapper for all fields
<AutoForm
  uiComponents={{
    FieldWrapper: CustomFieldWrapper,
  }}
/>;

// 2. Or override it for a specific field in your schema
const schema = z.object({
  email: z.string().check(
    fieldConfig({
      fieldWrapper: CustomFieldWrapper,
    }),
  ),
});

Adding new form components

You can also add your own custom field types. To do this, you need to extend the formComponents prop of your AutoForm component and add your custom field type.

Keep form components focused on wiring the input. Most form components should not render the label or error directly because FieldWrapper already handles that. Still AutoFormFieldProps includes them for advanced UI integrations.

// CustomInput.tsx
import { useController } from "react-hook-form";
import { AutoFormFieldProps } from "@acp-autoform/react"; // or your UI package

export function CustomInput({ id, inputProps }: AutoFormFieldProps) {
  const { field } = useController({ name: id });

  return (
    <div>
      <input
        id={id}
        type="text"
        className="bg-red-400 rounded-lg p-4"
        {...inputProps} // inputProps is configured in fieldConfig.
        {...field}
        value={field.value ?? ""}
      />
    </div>
  );
}

// App.tsx
import { CustomInput } from "./CustomInput";

<AutoForm
  formComponents={{
    custom: CustomInput,
  }}
/>;

// By default, AutoForm uses the Zod type to determine the input component to use.
// You can override this by using the fieldType property.
const schema = z.object({
  username: z.string().check(
    fieldConfig<React.ReactNode, FieldTypes | "custom">({
      fieldType: "custom",
    }),
  ),
});

For more examples on creating custom form components see examples.

Connecting custom fields to react-hook-form

AutoForm uses react-hook-form under the hood. To connect your custom fields, import useController hook from react-hook-form and use the id prop from AutoFormFieldProps as the name argument of useController.

import { useController } from "react-hook-form";
import { AutoFormFieldProps } from "@acp-autoform/react";

custom: ({ id, inputProps }: AutoFormFieldProps) => {
  const { field } = useController({
    name: id,
    disabled: inputProps?.disabled,
    rules: { required: true },
  });

  return <input id={id} {...inputProps} {...field} value={field.value ?? ""} />;
};

If you need fieldState (e.g. fieldState.invalid, fieldState.error) for component-specific styling or aria attributes, you can destructure it from useController:

import { useController } from "react-hook-form";

custom: ({ id, inputProps }: AutoFormFieldProps) => {
  // Use fieldState when you need component-specific styling or aria attributes.
  // Note: AutoForm already renders field errors via FieldWrapper — reach for
  // fieldState only when the component itself needs to react to error state.
  const { field, fieldState } = useController({ name: id });

  return (
    <input
      id={id}
      aria-invalid={fieldState.invalid}
      {...inputProps}
      {...field}
      value={field.value ?? ""}
    />
  );
};

For non-standard inputs, adapt the value and change handler to the component API:

customCheckbox: ({ id }: AutoFormFieldProps) => {
  const { field } = useController({ name: id });

  return (
    <input
      id={id}
      name={field.name}
      ref={field.ref}
      type="checkbox"
      checked={!!field.value}
      onBlur={field.onBlur}
      onChange={(event) => field.onChange(event.target.checked)}
    />
  );
};

For UI-library implementations, see the official field components: Shadcn fields, MUI fields, Mantine fields and Ant fields.

Form element customization

In addition to overriding UI components, you can pass additional props to the underlying form element using the formProps prop:

<AutoForm
  schema={schemaProvider}
  onSubmit={handleSubmit}
  formProps={{
    className: "my-custom-form",
    "data-testid": "user-form",
    noValidate: true,
    onKeyDown: (e) => {
      if (e.key === "Enter") e.preventDefault();
    },
  }}
/>

This allows you to add custom classes, data attributes, or other properties to the form element.

Submit Button

1. Submit button inside AutoForm

The simplest way is to use the withSubmit prop which adds a default submit button:

<AutoForm schema={schemaProvider} onSubmit={handleSubmit} withSubmit />

For more control, pass a custom submit button as a child of AutoForm. Children are rendered below the form fields:

<AutoForm schema={schemaProvider} onSubmit={handleSubmit}>
  <button type="submit" className="btn-primary">
    Create Account
  </button>
</AutoForm>

External submit button, using HTML form attribute

<AutoForm
  schema={schemaProvider}
  onSubmit={handleSubmit}
  formProps={{ id: "my-form" }}
/>
<button type="submit" form="my-form">
  Submit
</button>;

2. Submit and reset buttons outside AutoForm

Use createFormControl from react-hook-form to control the form externally. This lets you place submit, reset, or any other controls anywhere in your UI.

Create the form control once per form instance. In React components, use useMemo so it is not recreated on every render:

import * as React from "react";
import { createFormControl } from "react-hook-form";

export default function MyForm() {
  const schemaProvider = new ZodProvider(mySchema);

  const { formControl, handleSubmit, reset } = React.useMemo(
    () => createFormControl(),
    [],
  );

  return (
    <div>
      <AutoForm formControl={formControl} schema={schemaProvider} />

      <div className="flex gap-2 mt-4">
        <button
          type="button"
          onClick={handleSubmit((data) => {
            console.log("Validated data:", data);
          })}
        >
          Submit
        </button>
        <button type="button" onClick={() => reset()}>
          Reset
        </button>
      </div>
    </div>
  );
}

createFormControl requires react-hook-form version 7.55.0 or later.

Custom Resolver & Value Cleanup

AutoForm automatically cleans empty values such as "", null before validation. This ensures optional schema fields behave exactly as expected without triggering validation errors for empty inputs. If you want to change this default validation and value cleanup, you can provide your own resolver to createFormControl.

// example using zod resolver from @hookform/resolvers
// this overrides the default resolver and its automatic empty-value cleanup

import * as React from "react";
import { createFormControl } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { mySchema } from "@/lib/schema";

export default function MyForm() {
  const { formControl } = React.useMemo(
    () =>
      createFormControl({
        resolver: zodResolver(mySchema),
      }),
    [],
  );

  return <AutoForm formControl={formControl} schema={schemaProvider} />;
}

On this page