Dynamic Fields
10+ field types with conditional rendering and dynamic validation
A powerful dynamic form builder that combines React Hook Form, Zod validation, and shadcn/ui components for creating complex, validated forms with minimal code.
React Hook Form Zod Validation Type SafeDynamic Fields
10+ field types with conditional rendering and dynamic validation
Schema Validation
Zod-powered validation with real-time error handling and custom messages
Flexible Layout
Multi-column layouts, field groups, and responsive design options
Accessibility
ARIA labels, keyboard navigation, and screen reader support
Custom Fields
Extensible architecture for adding custom field components
Form State
Advanced state management with dirty tracking and auto-save
Create a form with validation in just a few lines:
import React from 'react'import { HiForm } from '@/components/HiForm'import { z } from 'zod'import type { HiFormField } from '@/components/HiForm/types'
// Define validation schemaconst userSchema = z.object({name: z.string().min(2, 'Name must be at least 2 characters'),email: z.string().email('Invalid email address'),age: z.number().min(18, 'Must be 18 or older'),role: z.string().min(1, 'Role is required'),isActive: z.boolean()})
// Define form fieldsconst userFields: HiFormField[] = [{ type: 'input', name: 'name', label: 'Full Name', placeholder: 'Enter your full name', required: true},{ type: 'input', name: 'email', label: 'Email Address', inputType: 'email', placeholder: 'Enter your email', required: true},{ type: 'input', name: 'age', label: 'Age', inputType: 'number', placeholder: 'Enter your age', required: true},{ type: 'select', name: 'role', label: 'Role', placeholder: 'Select a role', options: [ { label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }, { label: 'Manager', value: 'manager' } ], required: true},{ type: 'switch', name: 'isActive', label: 'Active Status', description: 'Enable or disable user account'}]
export default function UserForm() {const handleSubmit = (data: z.infer<typeof userSchema>) => { console.log('Form submitted:', data) // Handle form submission}
return ( <HiForm fields={userFields} schema={userSchema} onSubmit={handleSubmit} layout={{ columns: 2 }} buttons={{ submitText: 'Create User', cancelText: 'Cancel' }} />)}
// Text Input{type: 'input',name: 'username',label: 'Username',inputType: 'text', // text, email, password, number, tel, urlplaceholder: 'Enter username',required: true,autoComplete: 'username'}
// Password with confirmation{type: 'input',name: 'password',label: 'Password',inputType: 'password',placeholder: 'Enter password',required: true,showPasswordToggle: true}
// Number with constraints{type: 'input',name: 'price',label: 'Price',inputType: 'number',min: 0,max: 10000,step: 0.01,placeholder: '0.00'}
// Select Dropdown{type: 'select',name: 'category',label: 'Category',placeholder: 'Choose category',options: [ { label: 'Electronics', value: 'electronics' }, { label: 'Clothing', value: 'clothing' }, { label: 'Books', value: 'books' }],required: true}
// Combobox (Searchable Select){type: 'combobox',name: 'country',label: 'Country',placeholder: 'Search country...',options: [ { label: 'United States', value: 'us' }, { label: 'United Kingdom', value: 'uk' }, { label: 'Canada', value: 'ca' }],searchable: true,required: true}
// Radio Group{type: 'radio',name: 'plan',label: 'Subscription Plan',options: [ { label: 'Basic', value: 'basic', description: 'Perfect for individuals' }, { label: 'Pro', value: 'pro', description: 'Great for small teams' }, { label: 'Enterprise', value: 'enterprise', description: 'For large organizations' }],layout: 'vertical' // or 'horizontal'}
// Checkbox Group{type: 'checkbox',name: 'permissions',label: 'Permissions',options: [ { label: 'Read', value: 'read' }, { label: 'Write', value: 'write' }, { label: 'Delete', value: 'delete' }, { label: 'Admin', value: 'admin' }],layout: 'grid', // or 'vertical', 'horizontal'columns: 2}
// Date Picker{type: 'datePicker',name: 'birthdate',label: 'Birth Date',placeholder: 'Select date',required: true,disabledDates: { after: new Date(), // No future dates before: new Date('1900-01-01')}}
// Textarea{type: 'textarea',name: 'description',label: 'Description',placeholder: 'Enter description...',rows: 4,maxLength: 500,showCharacterCount: true}
// Slider{type: 'slider',name: 'budget',label: 'Budget Range',min: 1000,max: 100000,step: 1000,defaultValue: [10000, 50000],formatLabel: (value) => `$${value.toLocaleString()}`}
// Switch Toggle{type: 'switch',name: 'notifications',label: 'Email Notifications',description: 'Receive notifications via email'}
// Custom Field{type: 'custom',name: 'avatar',label: 'Profile Picture',component: AvatarUploadField,props: { maxSize: '2MB', acceptedTypes: ['image/jpeg', 'image/png']}}
import React from 'react'import { HiForm } from '@/components/HiForm'import { z } from 'zod'
// Dynamic schema based on user typeconst createUserSchema = (userType: string) => {const baseSchema = z.object({ name: z.string().min(2), email: z.string().email(), userType: z.enum(['individual', 'business'])})
if (userType === 'business') { return baseSchema.extend({ companyName: z.string().min(1, 'Company name is required'), taxId: z.string().min(1, 'Tax ID is required'), employeeCount: z.number().min(1) })}
return baseSchema.extend({ dateOfBirth: z.date().refine( (date) => date < new Date(), 'Date must be in the past' )})}
export default function ConditionalForm() {const [userType, setUserType] = React.useState<'individual' | 'business'>('individual')
const schema = React.useMemo(() => createUserSchema(userType), [userType])
const fields: HiFormField[] = [ { type: 'input', name: 'name', label: 'Full Name', required: true }, { type: 'input', name: 'email', label: 'Email', inputType: 'email', required: true }, { type: 'select', name: 'userType', label: 'Account Type', options: [ { label: 'Individual', value: 'individual' }, { label: 'Business', value: 'business' } ], required: true, onChange: (value) => setUserType(value as any) },
// Conditional fields based on userType ...(userType === 'business' ? [ { type: 'input', name: 'companyName', label: 'Company Name', required: true }, { type: 'input', name: 'taxId', label: 'Tax ID', required: true }, { type: 'input', name: 'employeeCount', label: 'Number of Employees', inputType: 'number', min: 1 } ] : [ { type: 'datePicker', name: 'dateOfBirth', label: 'Date of Birth', required: true } ])] as HiFormField[]
return ( <HiForm fields={fields} schema={schema} onSubmit={(data) => console.log('Submitted:', data)} layout={{ columns: 2 }} />)}
import { z } from 'zod'
// Custom validation functionsconst passwordSchema = z.string().min(8, 'Password must be at least 8 characters').regex(/[A-Z]/, 'Password must contain at least one uppercase letter').regex(/[a-z]/, 'Password must contain at least one lowercase letter').regex(/[0-9]/, 'Password must contain at least one number').regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character')
const uniqueEmailSchema = z.string().email('Invalid email format').refine( async (email) => { // Check if email is unique (API call) const exists = await checkEmailExists(email) return !exists }, 'Email is already taken')
// Complex form schemaconst registrationSchema = z.object({email: uniqueEmailSchema,password: passwordSchema,confirmPassword: z.string(),terms: z.boolean().refine((val) => val === true, { message: 'You must accept the terms and conditions'})}).refine((data) => data.password === data.confirmPassword, {message: 'Passwords do not match',path: ['confirmPassword']})
// Field with custom validationconst fields: HiFormField[] = [{ type: 'input', name: 'username', label: 'Username', required: true, validate: async (value) => { if (value.length < 3) { return 'Username must be at least 3 characters' }
const available = await checkUsernameAvailability(value) if (!available) { return 'Username is not available' }
return true }, debounceMs: 500 // Debounce async validation}]
// Two-column layout<HiFormfields={fields}schema={schema}layout={{ columns: 2, spacing: 'lg' // xs, sm, md, lg, xl}}/>
// Responsive layout<HiFormfields={fields}schema={schema}layout={{ columns: { default: 1, md: 2, lg: 3 }, spacing: 'md'}}/>
// Field groups with custom layoutsconst fieldGroups: HiFormFieldGroup[] = [{ title: 'Personal Information', description: 'Basic details about the user', fields: [ { type: 'input', name: 'firstName', label: 'First Name' }, { type: 'input', name: 'lastName', label: 'Last Name' }, { type: 'input', name: 'email', label: 'Email' } ], layout: { columns: 2 }},{ title: 'Address', fields: [ { type: 'input', name: 'street', label: 'Street Address', span: 2 }, { type: 'input', name: 'city', label: 'City' }, { type: 'input', name: 'zipCode', label: 'ZIP Code' } ], layout: { columns: 2 }}]
<HiFormfieldGroups={fieldGroups}schema={schema}onSubmit={handleSubmit}/>
// Custom form styling<HiFormfields={fields}schema={schema}styling={{ size: 'lg', // sm, md, lg variant: 'outline', // default, outline, ghost spacing: 'comfortable', // compact, comfortable, spacious labelPosition: 'top', // top, left, floating showRequiredIndicator: true, showOptionalIndicator: false}}className="max-w-2xl mx-auto p-6"/>
// Dark mode support<HiFormfields={fields}schema={schema}theme={{ colors: { primary: 'hsl(var(--primary))', background: 'hsl(var(--background))', border: 'hsl(var(--border))' }}}/>
import React from 'react'import { HiForm } from '@/components/HiForm'import { useForm } from 'react-hook-form'
export default function AdvancedFormState() {const [formData, setFormData] = React.useState(null)const [isDirty, setIsDirty] = React.useState(false)const [isSubmitting, setIsSubmitting] = React.useState(false)
const handleFormChange = (data: any, formState: any) => { setFormData(data) setIsDirty(formState.isDirty)}
const handleSubmit = async (data: any) => { setIsSubmitting(true) try { await submitData(data) // Handle success } catch (error) { // Handle error } finally { setIsSubmitting(false) }}
return ( <div> {isDirty && ( <div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded"> You have unsaved changes </div> )}
<HiForm fields={fields} schema={schema} onSubmit={handleSubmit} onChange={handleFormChange} defaultValues={initialData} buttons={{ submitText: isSubmitting ? 'Saving...' : 'Save', submitDisabled: isSubmitting, showCancel: true, cancelText: 'Reset', onCancel: () => { // Reset form } }} autoSave={{ enabled: true, debounceMs: 2000, onSave: (data) => { // Auto-save functionality localStorage.setItem('formDraft', JSON.stringify(data)) } }} /> </div>)}
import React from 'react'import { useFormContext } from 'react-hook-form'import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
interface CustomFieldProps {name: stringlabel: string// ... other props}
// Custom Rich Text Fieldconst RichTextField: React.FC<CustomFieldProps> = ({ name, label, ...props }) => {const form = useFormContext()
return ( <FormField control={form.control} name={name} render={({ field }) => ( <FormItem> <FormLabel>{label}</FormLabel> <FormControl> <RichTextEditor {...field} {...props} /> </FormControl> <FormMessage /> </FormItem> )} />)}
// Custom File Upload Fieldconst FileUploadField: React.FC<CustomFieldProps> = ({ name, label, accept, maxSize }) => {const form = useFormContext()
return ( <FormField control={form.control} name={name} render={({ field: { onChange, value, ...field } }) => ( <FormItem> <FormLabel>{label}</FormLabel> <FormControl> <div className="border-2 border-dashed border-gray-300 rounded-lg p-6"> <input type="file" accept={accept} onChange={(e) => { const file = e.target.files?.[0] if (file && file.size <= maxSize) { onChange(file) } }} {...field} /> </div> </FormControl> <FormMessage /> </FormItem> )} />)}
// Register custom fieldsconst customFields = {richText: RichTextField,fileUpload: FileUploadField}
// Use in formconst fields: HiFormField[] = [{ type: 'custom', name: 'content', label: 'Article Content', component: 'richText', props: { placeholder: 'Write your article...', maxLength: 5000 }},{ type: 'custom', name: 'thumbnail', label: 'Thumbnail Image', component: 'fileUpload', props: { accept: 'image/*', maxSize: 2 * 1024 * 1024 // 2MB }}]
<HiFormfields={fields}customFields={customFields}schema={schema}/>
import React, { useState } from 'react'import { HiForm } from '@/components/HiForm'import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'import { Button } from '@/components/ui/button'
export default function UserModal() {const [isOpen, setIsOpen] = useState(false)const [mode, setMode] = useState<'create' | 'edit'>('create')const [selectedUser, setSelectedUser] = useState<User | null>(null)
const handleCreate = () => { setMode('create') setSelectedUser(null) setIsOpen(true)}
const handleEdit = (user: User) => { setMode('edit') setSelectedUser(user) setIsOpen(true)}
const handleSubmit = async (data: any) => { if (mode === 'create') { await createUser(data) } else { await updateUser(selectedUser!.id, data) } setIsOpen(false) // Refresh user list}
return ( <> <Button onClick={handleCreate}>Add User</Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}> <DialogContent className="max-w-2xl"> <DialogHeader> <DialogTitle> {mode === 'create' ? 'Create User' : 'Edit User'} </DialogTitle> </DialogHeader>
<HiForm fields={userFields} schema={userSchema} defaultValues={selectedUser || {}} onSubmit={handleSubmit} layout={{ columns: 2 }} buttons={{ submitText: mode === 'create' ? 'Create' : 'Update', cancelText: 'Cancel', onCancel: () => setIsOpen(false) }} /> </DialogContent> </Dialog> </>)}
Prop | Type | Description |
---|---|---|
fields | HiFormField[] | Array of field definitions |
fieldGroups | HiFormFieldGroup[] | Grouped field definitions |
schema | ZodSchema | Zod validation schema |
defaultValues | object | Initial form values |
onSubmit | (data) => void | Submit handler |
onChange | (data, state) => void | Change handler |
layout | FormLayout | Layout configuration |
buttons | FormButtons | Button configuration |
styling | FormStyling | Styling options |
customFields | object | Custom field components |
interface HiFormField {type: 'input' | 'select' | 'textarea' | 'datePicker' | 'switch' | 'checkbox' | 'radio' | 'slider' | 'combobox' | 'custom'name: stringlabel: stringplaceholder?: stringdescription?: stringrequired?: booleandisabled?: booleanhidden?: boolean
// Layoutspan?: number // Column span (1-12)order?: number // Field order
// Validationvalidate?: (value: any) => boolean | string | Promise<boolean | string>debounceMs?: number
// Event handlersonChange?: (value: any) => voidonBlur?: () => voidonFocus?: () => void
// Field-specific propsoptions?: Array<{label: string, value: any}>min?: numbermax?: numberstep?: numberrows?: numbermaxLength?: number
// Custom fieldcomponent?: string | React.ComponentTypeprops?: object}
HiForm provides a comprehensive solution for building complex, validated forms with excellent user experience! 📝