Custom Components
UniForm ships with defaultRegistry — a minimal set of field components that render a <input>, <select>, and <input type="checkbox">. In production you will almost always replace these with your own design-system components.
The component registry
The registry maps a type key to a React component. The built-in keys are string, number, boolean, date, select (for z.enum() / z.nativeEnum(), or a string field with meta.options), and textarea (opt-in). You can add your own keys (e.g. "slider", "rating") and reference them via fields={{ myField: { component: 'rating' } }}.
Select from a string field
You can render a z.string() field as a select by setting meta.component: 'select' and providing meta.options. UniForm will treat the field as type "select" during introspection and pass the options to your select component:
const schema = z.object({
role: z
.string()
.meta({ component: 'select', options: [
{ label: 'User', value: 'user' },
{ label: 'Admin', value: 'admin' },
{ label: 'Editor', value: 'editor' },
] }),
})
This is an alternative to z.enum(['user', 'admin', 'editor']) — useful when the option list is defined at runtime, or when you want a plain string in the output type rather than a union literal.
You can override any key without replacing the others — your registry is merged with defaultRegistry. For the full type definition and resolution order see ComponentRegistry in the API reference.
Writing a custom component
Every field component receives FieldProps:
import type { FieldProps } from '@uniform-ts/core'
export function StarRating({ value, onChange, error }: FieldProps) {
return (
<div>
{[1, 2, 3, 4, 5].map((star) => (
<button
type='button'
key={star}
onClick={() => onChange(star)}
style={{
color: (Number(value) || 0) >= star ? 'gold' : 'gray',
fontSize: 24,
background: 'none',
border: 'none',
cursor: 'pointer',
}}
>
★
</button>
))}
{error && <p style={{ color: 'red', fontSize: 12 }}>{error}</p>}
</div>
)
}
Then register it and point the field at it:
const myRegistry = { rating: StarRating }
<AutoForm components={myRegistry} fields={{ score: { component: 'rating' } }} ... />
To replace a built-in type for all fields of that type in a form, register it under the type key:
// Every z.string() field now uses MyTextInput
<AutoForm components={{ string: MyTextInput }} ... />
To replace it for a single field only, pass the component directly in fields:
fields={{ bio: { component: MyTextarea } }}
Live Example
const StarRating = ({ value, onChange, error }) => ( <div> {[1, 2, 3, 4, 5].map((star) => ( <button type='button' key={star} onClick={() => onChange(star)} style={{ color: (Number(value) || 0) >= star ? 'gold' : 'var(--ifm-color-emphasis-400)', fontSize: 28, background: 'none', border: 'none', cursor: 'pointer', padding: '0 2px', }} > ★ </button> ))} {error && ( <p style={{ color: 'var(--ifm-color-danger)', fontSize: 12, margin: '4px 0 0', }} > {error} </p> )} </div> ) const schema = z.object({ productName: z.string().min(1, 'Required'), rating: z.number().min(1, 'Please rate the product').max(5), review: z.string().optional(), }) const reviewForm = createForm(schema) function App() { const [result, setResult] = React.useState(null) return ( <div style={{ fontFamily: 'system-ui', maxWidth: 420 }}> <AutoForm form={reviewForm} components={{ rating: StarRating }} fields={{ productName: { label: 'Product' }, rating: { label: 'Your rating', component: 'rating' }, review: { label: 'Written review' }, }} onSubmit={(v) => setResult(v)} /> {result && ( <pre style={{ marginTop: '1rem', background: 'var(--ifm-color-emphasis-200)', padding: '1rem', borderRadius: 6, }} > {JSON.stringify(result, null, 2)} </pre> )} </div> ) } render(<App />)