Skip to content

HiForm Component

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 Safe

Dynamic 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:

Basic HiForm Usage
import React from 'react'
import { HiForm } from '@/components/HiForm'
import { z } from 'zod'
import type { HiFormField } from '@/components/HiForm/types'
// Define validation schema
const 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 fields
const 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'
}}
/>
)
}
Input Field Types
// Text Input
{
type: 'input',
name: 'username',
label: 'Username',
inputType: 'text', // text, email, password, number, tel, url
placeholder: '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'
}
Selection Field Types
// 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
}
Advanced Field Types
// 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']
}
}
Conditional Fields Example
import React from 'react'
import { HiForm } from '@/components/HiForm'
import { z } from 'zod'
// Dynamic schema based on user type
const 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 }}
/>
)
}
Custom Validation
import { z } from 'zod'
// Custom validation functions
const 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 schema
const 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 validation
const 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
}
]
Layout Configuration
// Two-column layout
<HiForm
fields={fields}
schema={schema}
layout={{
columns: 2,
spacing: 'lg' // xs, sm, md, lg, xl
}}
/>
// Responsive layout
<HiForm
fields={fields}
schema={schema}
layout={{
columns: {
default: 1,
md: 2,
lg: 3
},
spacing: 'md'
}}
/>
// Field groups with custom layouts
const 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 }
}
]
<HiForm
fieldGroups={fieldGroups}
schema={schema}
onSubmit={handleSubmit}
/>
Form Styling
// Custom form styling
<HiForm
fields={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
<HiForm
fields={fields}
schema={schema}
theme={{
colors: {
primary: 'hsl(var(--primary))',
background: 'hsl(var(--background))',
border: 'hsl(var(--border))'
}
}}
/>
Form State Management
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>
)
}
Custom Field Components
import React from 'react'
import { useFormContext } from 'react-hook-form'
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
interface CustomFieldProps {
name: string
label: string
// ... other props
}
// Custom Rich Text Field
const 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 Field
const 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 fields
const customFields = {
richText: RichTextField,
fileUpload: FileUploadField
}
// Use in form
const 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
}
}
]
<HiForm
fields={fields}
customFields={customFields}
schema={schema}
/>
Form in Modal
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>
</>
)
}
PropTypeDescription
fieldsHiFormField[]Array of field definitions
fieldGroupsHiFormFieldGroup[]Grouped field definitions
schemaZodSchemaZod validation schema
defaultValuesobjectInitial form values
onSubmit(data) => voidSubmit handler
onChange(data, state) => voidChange handler
layoutFormLayoutLayout configuration
buttonsFormButtonsButton configuration
stylingFormStylingStyling options
customFieldsobjectCustom field components
Field Definition Interface
interface HiFormField {
type: 'input' | 'select' | 'textarea' | 'datePicker' | 'switch' | 'checkbox' | 'radio' | 'slider' | 'combobox' | 'custom'
name: string
label: string
placeholder?: string
description?: string
required?: boolean
disabled?: boolean
hidden?: boolean
// Layout
span?: number // Column span (1-12)
order?: number // Field order
// Validation
validate?: (value: any) => boolean | string | Promise<boolean | string>
debounceMs?: number
// Event handlers
onChange?: (value: any) => void
onBlur?: () => void
onFocus?: () => void
// Field-specific props
options?: Array<{label: string, value: any}>
min?: number
max?: number
step?: number
rows?: number
maxLength?: number
// Custom field
component?: string | React.ComponentType
props?: object
}

HiForm provides a comprehensive solution for building complex, validated forms with excellent user experience! 📝