Form Wire
Guides

Field Types

All supported field types and how to configure them

Form Wire maps Zod types to form controls automatically. Here's every supported type.

Text Input

z.string().min(1, "Required")

Renders as <input type="text">. Override with the component option:

fields: {
  bio: { label: "Bio", component: "textarea" },
}

Email

z.email("Enter a valid email")

Renders as <input type="email">.

Number

z.coerce.number().min(18, "Must be 18+")

Renders as <input type="number">. Uses z.coerce so string form values are coerced to numbers.

Date

z.coerce.date()

Renders as <input type="date">.

Boolean / Checkbox

z.boolean()

Renders as <input type="checkbox">.

For "must accept" patterns:

z.literal(true, "You must accept the terms")

This renders a checkbox that must be checked to submit.

Select

z.enum(["pending", "active", "paused"])

Renders as <select> with options. Optional selects include a blank placeholder option.

Configure the placeholder:

fields: {
  status: { label: "Status", placeholder: "Choose a status" },
}

Use options when labels need punctuation, branding, or copy that should not come from enum values:

const CompanyForm = createFormWire(schema, {
  fields: {
    status: {
      options: [
        { label: "Draft / internal", value: "pending" },
        { label: "Active (public)", value: "active" },
        { label: "Paused - billing", value: "paused" },
      ],
    },
  },
});

For query-backed selects, pass options at render time:

<CompanyForm.Field
  name="companyId"
  options={companies.map((company) => ({
    label: company.displayName,
    value: company.id,
  }))}
/>

File

z.file()

Renders as <input type="file">.

Common file flows:

// Parse locally inside the action.
const schema = z.object({
  csv: z.file().mime("text/csv"),
});

export const importCsv = createAction(schema, async ({ csv }) => {
  const text = await csv.text();
  await importRows(text);
  return { message: "Imported" };
});
// Upload to storage inside the action.
const schema = z.object({
  attachment: z.file().max(10_000_000),
});

export const uploadAttachment = createAction(schema, async ({ attachment }) => {
  const key = await storage.put(attachment.name, await attachment.arrayBuffer(), {
    contentType: attachment.type,
  });

  return { data: { key } };
});

For custom file UI, keep Form Wire's file mapper responsible for the actual file input so the submitted FormData still includes the selected file:

const mapper: FormWireMapper = {
  ...htmlMapper,
  file: (props) => (
    <label>
      <span>{props.field.label}</span>
      <input
        id={props.id}
        name={props.name}
        type="file"
        required={props.required}
        disabled={props.disabled}
        aria-describedby={props.describedBy}
        aria-invalid={props.invalid}
      />
    </label>
  ),
};

String Arrays

z.string().array().min(1)

Renders as a dynamic list of text inputs with add/remove/reorder controls.

Configure labels:

fields: {
  tags: {
    label: "Tags",
    array: {
      addLabel: "Add tag",
      removeLabel: "Remove",
      itemLabel: "Tag",
    },
  },
}

Object Arrays

z.object({
  name: z.string(),
  email: z.email(),
}).array()

Renders as fieldset groups with add/remove/reorder controls for each item.

Custom Component Override

Use component in field config to override the default rendering:

fields: {
  bio: { component: "textarea" },
}

Currently supported: "textarea". For full custom rendering, use a custom mapper.

On this page