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" },
}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.