Widget Reference
Formura infers the right widget from your Zod schema. Use .describe("widget:<name>") when inference is ambiguous.
Inference table
Formura reads your Zod field type — and optionally a .describe() hint — to choose the correct widget automatically. Optional, nullable, and default wrappers are stripped before inference.
| Zod type | Widget |
|---|---|
| z.string() | text |
| z.string().describe("widget:password") | password |
| z.string().describe("widget:otp") or fixed length 4–8 | otp |
| z.string().describe("widget:textarea") | textarea |
| z.number() | number |
| z.enum([...]) | select |
| z.array(z.enum([...])) | multiSelect |
| z.boolean() | checkbox |
| z.coerce.date() / z.date() | date |
| z.string().describe("widget:time") | time |
| z.coerce.date().describe("widget:datetime") | datetime |
Widget hints
Add .describe("widget:<name>") to any Zod field to override or guide inference. Formura parses the widget: prefix from the description string using a regex — any other description content is ignored. Valid widget names are:
// Explicit widget hints
password: z.string().min(8).describe("widget:password")
bio: z.string().max(500).describe("widget:textarea")
code: z.string().length(6).describe("widget:otp")
start: z.string().describe("widget:time")
meeting: z.coerce.date().describe("widget:datetime")The description string can contain other text — only the widget: portion is parsed. For example, .describe("User bio widget:textarea") works fine.
Optional, nullable, and default
Formura unwraps Zod modifier wrappers before inferring the widget. You can mark fields optional or give them defaults without affecting the rendered widget:
// All of these still render a "text" widget
username: z.string()
username: z.string().optional()
username: z.string().nullable()
username: z.string().default("")
// Wrappers are stripped up to 16 layers deep
username: z.string().optional().default("").nullable()Per-widget examples
username: z.string().min(2, "At least 2 characters")Default for all strings without a widget hint. Renders a plain text input.
password: z.string().min(8).describe("widget:password")Renders an input with type="password". Inference won't guess this on its own — always add the hint.
code: z.string().length(6).describe("widget:otp")
// Auto-inferred when minLength === maxLength and the value is 4–8
verificationPin: z.string().min(6).max(6)OTP length is read from the schema's min/max constraints. Falls back to 6 slots when ambiguous.
bio: z.string().max(500).describe("widget:textarea")Renders a resizable textarea. Zod max() acts as a visual guide — HTML maxLength is not set automatically.
age: z.number().min(18).max(120)Renders type="number". The field value is a JavaScript number, not a string — no manual coercion needed.
role: z.enum(["developer", "designer", "manager"])Options are derived from the enum values. Labels match values by default — use the fields config to rename them.
skills: z.array(z.enum(["react", "node", "design"]))Renders a checkbox list. Value is a string array matching the selected enum values.
terms: z.boolean().refine((v) => v, "You must accept the terms")Renders a single checkbox. Value is a native boolean.
birthday: z.coerce.date()
// z.coerce.date() is preferred — it accepts ISO strings from FormDataRenders a calendar date picker. Value is a JavaScript Date object with the time component set to midnight.
startTime: z.string().describe("widget:time")Value is a string in "HH:mm" 24-hour format (e.g. "14:30"). Inference will not guess time from z.string() alone — the hint is required.
scheduledAt: z.coerce.date().describe("widget:datetime")Renders a combined calendar + time picker. Value is a JavaScript Date object with both date and time components.
Date and time value types
Date and time widgets each have a distinct value type. Make sure your schema matches:
| Widget | Value type | Example value |
|---|---|---|
| date | Date | new Date("2026-07-08") |
| time | string | "14:30" |
| datetime | Date | new Date("2026-07-08T14:30:00") |
Use z.coerce.date() instead of z.date() when the value may arrive as an ISO string (e.g. from a Server Action via FormData).
Custom shadcn components
Extend the default adapter when you need to swap primitives from your local shadcn install. See Adapters.
import { createShadcnAdapter } from "@formura/adapters/shadcn";
const adapter = createShadcnAdapter();Next
Adapters
Tailwind setup, default shadcn adapter, and custom adapters.