Array Fields
z.array(z.object(...)) fields are automatically rendered as a repeating group. Each row is an independent nested form segment rendered below an Add button.
UniForm renders array fields whose item schema is a z.object(...). Arrays of primitives (e.g. z.array(z.string())) are not rendered as repeating fields — use a custom component for those cases.
const schema = z.object({
members: z.array(
z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['owner', 'member', 'guest']),
}),
),
})
Row controls
By default each row gets:
| Control | Behaviour |
|---|---|
| Remove | Removes that row from the array |
| Move Up / Move Down | Reorders rows |
Enable Duplicate and Collapse per-row via the fields prop:
<AutoForm
fields={{
members: { duplicable: true, collapsible: true },
}}
...
/>
You can also replace the entire row layout via layout.arrayRowLayout. The component receives children (the row's fields) and a buttons object containing pre-rendered button nodes — place them wherever you like:
const MyRowLayout = ({ children, buttons, index }) => (
<div className='array-row'>
{buttons.collapse}
{children}
<div className='row-controls'>
{buttons.moveUp}
{buttons.moveDown}
{buttons.duplicate}
{buttons.remove}
</div>
</div>
)
See ArrayRowLayoutProps for the full type.
Labels
Override the Add / Remove button labels via the labels prop for i18n:
<AutoForm labels={{ arrayAdd: '+ Add member', arrayRemove: 'Remove' }} ... />
Minimum / maximum items
UniForm respects .min(n) and .max(n) on z.array(...):
z.array(memberSchema)
.min(1, 'At least 1 member required')
.max(10, 'Maximum 10 members')
The Add button is hidden when the max is reached.
Live Example
const memberSchema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Invalid email'), role: z.enum(['owner', 'member', 'guest']), }) const teamSchema = z.object({ teamName: z.string().min(1, 'Required'), members: z.array(memberSchema).min(1, 'Add at least one member'), }) const teamForm = createForm(teamSchema) const CompactRowLayout = ({ children, buttons, index }) => ( <div style={{ border: '1px solid var(--ifm-color-emphasis-300)', borderRadius: 8, padding: '0.75rem', marginBottom: '0.5rem', background: index % 2 === 0 ? 'var(--ifm-color-emphasis-100)' : 'var(--ifm-background-color)', }} > {children} <div style={{ display: 'flex', gap: 8, marginTop: 8, justifyContent: 'flex-end', }} > {buttons.moveUp} {buttons.moveDown} {buttons.duplicate} {buttons.remove} </div> </div> ) function App() { const [result, setResult] = React.useState(null) return ( <div style={{ fontFamily: 'system-ui', maxWidth: 480 }}> <style>{`.ar-dup { color: var(--ifm-color-primary) } .ar-rm { color: var(--ifm-color-danger) }`}</style> <AutoForm form={teamForm} defaultValues={{ members: [{ name: '', email: '', role: 'member' }] }} fields={{ teamName: { label: 'Team Name' }, members: { label: 'Team Members', movable: true, duplicable: true }, }} classNames={{ arrayDuplicate: 'ar-dup', arrayRemove: 'ar-rm' }} layout={{ arrayRowLayout: CompactRowLayout }} labels={{ arrayAdd: '+ Add member', submit: 'Create Team' }} onSubmit={(v) => setResult(v)} /> {result && ( <pre style={{ marginTop: '1rem', background: 'var(--ifm-color-emphasis-200)', padding: '1rem', borderRadius: 6, fontSize: 12, }} > {JSON.stringify(result, null, 2)} </pre> )} </div> ) } render(<App />)