A comprehensive, type-safe form component library built with React Hook Form, Zod validation, and shadcn/ui components. Designed for Next.js applications with clean, reusable architecture.
- Features
- Installation
- Quick Start
- Available Components
- Component API
- Examples
- Customization
- TypeScript Support
- Error Handling
- Best Practices
- Contributing
- Type-Safe: Full TypeScript support with Zod schema validation
- Single Input Component: One
RHFInputcomponent handles all input types - Clean API: Minimal, readable form definitions
- Server-Side Ready: Built for Next.js Server Actions and API routes
- Flexible Architecture: Easy to extend and customize
- Accessible: Built on shadcn/ui components with accessibility in mind
- Automatic Validation: Real-time validation with proper error handling
- Debug Mode: Built-in debug panel for development
Make sure you have the following dependencies installed:
bun install react-hook-form @hookform/resolvers zod lucide-react- Copy the form components to your project structure:
your-project/
ββ components/
β ββ ui/
β β ββ button.tsx
β β ββ form.tsx
β β ββ input.tsx
β β ββ textarea.tsx
β β ββ select.tsx
β β ββ checkbox.tsx
β β ββ alert.tsx
β ββ form/
β ββ RHFormContainer.tsx
β ββ RHFInput.tsx
β ββ types.ts
- Ensure you have shadcn/ui components installed:
bunx --bun shadcn@latest init
bunx --bun shadcn@latest add button form input textarea select checkbox alertimport { z } from "zod";
const userSchema = z.object({
username: z
.string()
.min(2, "Username must be at least 2 characters")
.max(50, "Username must be less than 50 characters"),
email: z
.string()
.email("Please enter a valid email address"),
age: z
.number()
.min(18, "Age must be at least 18")
.max(120, "Age must be less than 120"),
role: z.enum(['admin', 'user', 'moderator'], {
errorMap: () => ({ message: 'Please select a role' }),
}),
bio: z
.string()
.max(500, "Bio must be less than 500 characters")
.optional(),
terms: z.boolean().refine((val) => val === true, {
message: "You must accept the terms and conditions",
}),
});
type UserFormData = z.infer<typeof userSchema>;'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { useState } from 'react'
import { RHFormContainer } from '@/components/form/RHFormContainer'
import { RHFInput } from '@/components/form/RHFInput'
import { userSchema, type UserFormData } from './schema'
export function UserProfileForm() {
const [submitStatus, setSubmitStatus] = useState<{
type: 'success' | 'error' | null
message: string
}>({ type: null, message: '' })
const form = useForm<UserFormData>({
resolver: zodResolver(userSchema),
defaultValues: {
username: '',
email: '',
age: 18,
bio: '',
terms: false,
},
mode: 'onChange',
})
const onSubmit = async (data: UserFormData) => {
try {
setSubmitStatus({ type: null, message: '' })
// Your API call or Server Action here
await new Promise((resolve) => setTimeout(resolve, 1000))
setSubmitStatus({
type: 'success',
message: 'User profile created successfully!',
})
form.reset()
} catch (error) {
setSubmitStatus({
type: 'error',
message: 'Something went wrong. Please try again.',
})
}
}
const roleOptions = [
{ value: 'user', label: 'User' },
{ value: 'moderator', label: 'Moderator' },
{ value: 'admin', label: 'Admin' },
]
return (
<div className="max-w-2xl mx-auto p-6">
<div className="space-y-2 mb-6">
<h1 className="text-2xl font-bold">Create User Profile</h1>
<p className="text-muted-foreground">
Fill out the form below to create a new user profile.
</p>
</div>
<RHFormContainer
form={form}
onSubmit={onSubmit}
submitText="Create Profile"
loadingText="Creating Profile..."
status={submitStatus}
showDebug={false}
>
<RHFInput
name="username"
control={form.control}
type="text"
label="Username"
placeholder="johndoe"
description="This will be your public display name."
/>
<RHFInput
name="email"
control={form.control}
type="email"
label="Email"
placeholder="john@example.com"
/>
<RHFInput
name="age"
control={form.control}
type="number"
label="Age"
placeholder="25"
description="You must be at least 18 years old."
/>
<RHFInput
name="role"
control={form.control}
type="select"
label="Role"
placeholder="Select a role"
description="Choose your role in the application."
options={roleOptions}
/>
<RHFInput
name="bio"
control={form.control}
type="textarea"
label="Bio"
placeholder="Tell us a little bit about yourself"
description="Optional. Maximum 500 characters."
rows={4}
/>
<RHFInput
name="terms"
control={form.control}
type="checkbox"
label="Accept terms and conditions"
description="You agree to our Terms of Service and Privacy Policy."
/>
</RHFormContainer>
</div>
)
}// app/actions.ts
"use server";
import { z } from "zod";
import { userSchema } from "./schema";
export async function createUser(data: z.infer<typeof userSchema>) {
// Server-side validation
const validatedData = userSchema.parse(data);
// Your server-side logic here
console.log("Creating user:", validatedData);
return { success: true, message: "User created successfully!" };
}// In your form component
import { createUser } from "./actions";
const onSubmit = async (data: UserFormData) => {
try {
const result = await createUser(data);
if (result.success) {
setSubmitStatus({
type: 'success',
message: result.message,
});
}
} catch (error) {
setSubmitStatus({
type: 'error',
message: 'Failed to create user.',
});
}
};RHFormContainer- Main form wrapper with submission handlingRHFInput- Universal input component supporting all input types
text- Standard text inputemail- Email input with validationpassword- Password inputnumber- Number input with automatic conversiontextarea- Multi-line text inputselect- Dropdown select with optionscheckbox- Checkbox input
interface RHFormContainerProps<TFieldValues extends FieldValues> {
form: UseFormReturn<TFieldValues>
onSubmit: SubmitHandler<TFieldValues>
children: ReactNode
submitText?: string // Default: "Submit"
loadingText?: string // Default: "Submitting..."
className?: string // Default: "space-y-6"
status?: FormStatus // For success/error alerts
showDebug?: boolean // Default: false
}// Base props for all input types
interface BaseInputProps<TFieldValues extends FieldValues> {
name: FieldPath<TFieldValues>
control: Control<TFieldValues>
label?: string
placeholder?: string
description?: string
disabled?: boolean
className?: string
}
// Type-specific props are automatically inferred based on the 'type' prop<RHFormContainer form={form} onSubmit={onSubmit}>
<RHFInput
name="name"
control={form.control}
type="text"
label="Full Name"
placeholder="Enter your name"
/>
<RHFInput
name="email"
control={form.control}
type="email"
label="Email"
placeholder="your@email.com"
/>
</RHFormContainer>const advancedSchema = z.object({
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Must contain uppercase letter")
.regex(/[0-9]/, "Must contain number"),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});const conditionalSchema = z.object({
hasAddress: z.boolean(),
address: z.string().optional(),
}).refine((data) => {
if (data.hasAddress) {
return data.address && data.address.length > 0;
}
return true;
}, {
message: "Address is required when selected",
path: ["address"],
});<RHFInput
name="customField"
control={form.control}
type="text"
label="Custom Field"
className="border-2 border-blue-500"
/>const customOptions = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
];
<RHFInput
name="selection"
control={form.control}
type="select"
label="Choose Option"
options={customOptions}
/>The components provide full TypeScript support with:
- Generic form data types from Zod schemas
- Discriminated unions for input type safety
- Automatic prop completion based on input type
- Compile-time error checking for invalid prop combinations
// TypeScript will enforce correct props based on input type
<RHFInput
name="age"
control={form.control}
type="number" // TypeScript knows this is a number input
// min, max props are available for number type
/>
<RHFInput
name="role"
control={form.control}
type="select" // TypeScript knows this is a select input
options={roleOptions} // options prop is required for select type
/>const onSubmit = async (data: FormData) => {
try {
await submitData(data);
} catch (error) {
setSubmitStatus({
type: 'error',
message: 'Submission failed. Please try again.',
});
}
};Validation errors are automatically handled by the Zod schema and displayed under each field.
- Always define Zod schemas for type safety and validation
- Use
mode: 'onChange'for real-time validation feedback - Provide helpful descriptions for complex fields
- Handle loading states in your submit handlers
- Use the debug mode during development
- Reset forms after successful submission
To extend this library:
- Add new input types to the
InputTypeunion intypes.ts - Create corresponding interfaces for new input props
- Add implementation in the
RHFInputcomponent switch statement - Update this README with examples and documentation
This component library is provided as-is for use in your projects. Feel free to modify and distribute according to your needs.
Happy form building! π