- Introduction
- Installation & Setup
- Core Concepts
- Form Components
- Input Types
- Advanced Features
- Theming
- Best Practices
- API Reference
- Examples
This is a comprehensive, production-ready React Hook Form component system built with TypeScript, Tailwind CSS, and Next.js. It provides a complete set of form components with built-in validation, error handling, dark mode support, and a clean API.
- ✅ Type-safe - Full TypeScript support with generics
- ✅ Zero boilerplate - Automatic form integration, no manual onChange needed
- ✅ Dark mode ready - Built-in theme support with next-themes
- ✅ Accessible - Follows WCAG guidelines
- ✅ Customizable - Easy to style and extend
- ✅ Production-ready - Battle-tested patterns and error handling
npm install react-hook-form
npm install next-themes # For dark mode supportcomponents/form/
├── Form.tsx # Main form wrapper
├── FormItem.tsx # Form field container
├── FormLabel.tsx # Label component
├── FormControl.tsx # Input wrapper
├── FormDescription.tsx # Helper text
├── FormMessage.tsx # Error messages
├── RHFCheckbox.tsx # Single checkbox
├── RHFCheckboxGroup.tsx # Multiple checkboxes
├── RHFSearchableCheckboxGroup.tsx # Searchable checkbox list
├── RHFRadioGroup.tsx # Radio button group
├── RHFUpload.tsx # File upload (single/multiple)
├── useThemeMode.ts # Theme management hook
├── index.ts # Exports all components
├── input/
│ ├── RHFInput.tsx # Text, email, password, number inputs
│ ├── RHFTextarea.tsx # Textarea input
│ ├── RHFOtp.tsx # OTP input
│ ├── RHFEditor.tsx # Rich text editor
│ └── index.ts
├── date/
│ ├── RHFDatePicker.tsx # Date picker
│ ├── RHFDateRangePicker.tsx # Date range picker
│ ├── RHFDateInput.tsx # Date input
│ ├── RHFTimeInput.tsx # Time input
│ └── index.ts
└── others/
├── RHFSelect.tsx # Dropdown (single/multi-select)
└── index.ts
- Wrap your app with ThemeProvider (in
app/layout.tsx):
import { ThemeProvider } from "@/components/ThemeProvider";
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}- Import form components:
import { useForm } from "react-hook-form";
import { Form, Field } from "@/components/form";Define your form data type first:
interface LoginFormData {
email: string;
password: string;
rememberMe: boolean;
}
const form = useForm<LoginFormData>({
defaultValues: {
email: "",
password: "",
rememberMe: false,
},
});All components use React Hook Form's Controller internally - no manual onChange needed:
// ❌ DON'T DO THIS (manual management)
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
// ✅ DO THIS (automatic management)
<Field.Input
name="email"
type="email"
label="Email"
/>Use the name prop to match your form schema. TypeScript will autocomplete available field names:
<Field.Input
name="email" // ← TypeScript knows this field exists
type="email"
label="Email Address"
/>Wrap all form fields in the <Form> component:
import { useForm } from "react-hook-form";
import { Form, Field } from "@/components/form";
function MyForm() {
const form = useForm<FormData>();
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<Form form={form} onSubmit={onSubmit} className="space-y-4">
{/* Your form fields here */}
</Form>
);
}Props:
form- React Hook Form instance (required)onSubmit- Submit handler (required)className- Additional CSS classeschildren- Form fields
HeroUI Input component for text, email, password, search, tel, url, and number types.
<Field.Input
name="firstName"
type="text"
label="First Name"
placeholder="John"
isRequired
description="Enter your legal first name"
/><Field.Input
name="email"
type="email"
label="Email Address"
placeholder="john@example.com"
isRequired
/><Field.Input
name="password"
type="password"
label="Password"
placeholder="Enter password"
isRequired
description="Must be at least 8 characters"
/><Field.Input
name="age"
type="number"
label="Age"
minValue={18}
maxValue={120}
step={1}
isRequired
/><Field.Input
name="phone"
type="tel"
label="Phone Number"
placeholder="+1 (555) 123-4567"
/><Field.Input
name="search"
type="search"
label="Search"
placeholder="Search..."
isClearable
/><Field.Input
name="website"
type="url"
label="Website"
placeholder="https://example.com"
/>HeroUI Textarea component for multi-line text input.
<Field.Textarea
name="bio"
label="Bio"
placeholder="Tell us about yourself..."
description="Maximum 500 characters"
minRows={4}
maxRows={8}
/>Props:
minRows- Minimum number of rowsmaxRows- Maximum number of rowsdisableAutosize- Disable automatic resizingminLength- Minimum character lengthmaxLength- Maximum character length
HeroUI InputOtp component for verification codes.
<Field.Otp
name="verificationCode"
label="Verification Code"
length={6}
allowedKeys="^[0-9]*$"
description="Enter the 6-digit code sent to your email"
/>Features:
- Individual character boxes
- Auto-focuses next box on entry
- Backspace navigates to previous box
- Arrow keys for navigation
- Paste support (pastes full code)
Props:
length- Number of characters (default: 6)allowedKeys- Regex pattern for allowed characterstextAlign- Text alignment in boxes
HeroUI DatePicker component for selecting dates.
<Field.DatePicker
name="birthDate"
label="Birth Date"
isRequired
showMonthAndYearPickers
/>Props:
showMonthAndYearPickers- Show month/year selectionvisibleMonths- Number of visible monthsminValue- Minimum selectable datemaxValue- Maximum selectable dategranularity- Time granularity (day, hour, minute, second)
HeroUI DateRangePicker component for selecting date ranges.
<Field.DateRangePicker
name="dateRange"
label="Date Range"
isRequired
showMonthAndYearPickers
/>HeroUI DateInput component for entering dates via text.
<Field.DateInput
name="startDate"
label="Start Date"
placeholderValue={new CalendarDate(2024, 1, 1)}
isRequired
/>HeroUI TimeInput component for entering times.
<Field.TimeInput name="appointmentTime" label="Appointment Time" isRequired />HeroUI Select component supporting single and multi-select.
<Field.Select
name="country"
label="Country"
options={[
{ key: "us", label: "United States", value: "us" },
{ key: "uk", label: "United Kingdom", value: "uk" },
{ key: "ca", label: "Canada", value: "ca" },
]}
placeholder="Select your country"
isRequired
description="Select the country where you reside"
/><Field.Select
name="skills"
label="Technical Skills"
options={[
{ key: "react", label: "React", value: "react" },
{ key: "typescript", label: "TypeScript", value: "typescript" },
{ key: "nodejs", label: "Node.js", value: "nodejs" },
]}
selectionMode="multiple"
description="Select your skills"
/><Field.Select
name="role"
label="Role"
sections={[
{
key: "admin",
title: "Admin Roles",
items: [
{ key: "superadmin", label: "Super Admin", value: "superadmin" },
{ key: "admin", label: "Admin", value: "admin" },
],
},
{
key: "user",
title: "User Roles",
items: [
{ key: "member", label: "Member", value: "member" },
{ key: "guest", label: "Guest", value: "guest" },
],
},
]}
/><Field.Select
name="role"
label="Role"
options={[
{ key: "admin", label: "Admin", value: "admin" },
{ key: "user", label: "User", value: "user" },
]}
disabledKeys={["admin"]}
/>Props:
options- Array of select optionssections- Grouped options with section headersselectionMode- "single" or "multiple"disabledKeys- Array of disabled option keysisLoading- Show loading stateisVirtualized- Enable virtualization for large lists
<Field.Checkbox
name="acceptTerms"
label="I accept the terms and conditions"
isRequired
/>
<Field.Checkbox
name="newsletter"
label="Subscribe to newsletter"
description="Receive updates about new features"
disabled={false}
/><Field.CheckboxGroup
name="preferences"
label="Email Preferences"
options={[
{ label: "Newsletter", value: "newsletter" },
{ label: "Product Updates", value: "updates" },
{ label: "Marketing Emails", value: "marketing" },
{ label: "Event Notifications", value: "events" },
]}
description="Choose the types of emails you'd like to receive"
/>With disabled options:
<Field.CheckboxGroup
name="features"
label="Features"
options={[
{ label: "Feature A", value: "a" },
{ label: "Feature B (coming soon)", value: "b", disabled: true },
]}
/>For large lists of options with search functionality.
<Field.SearchableCheckboxGroup
name="languages"
label="Programming Languages"
options={[
{ label: "JavaScript", value: "javascript" },
{ label: "TypeScript", value: "typescript" },
{ label: "Python", value: "python" },
// ... many more options
]}
placeholder="Search languages..."
maxHeight="300px" // Scrollable height
description="Search and select languages you know"
/>Features:
- Search input to filter options
- "Select All" and "Clear All" buttons
- Shows count of selected items
- Scrollable with custom max height
- "No results found" message
<Field.RadioGroup
name="gender"
label="Gender"
options={[
{ label: "Male", value: "male" },
{ label: "Female", value: "female" },
{ label: "Other", value: "other" },
]}
isRequired
/><Field.RadioGroup
name="preference"
label="Preference"
options={[...]}
horizontal // Display options side-by-side
/><Field.RadioGroup
name="paymentMethod"
label="Payment Method"
options={[
{
label: "Credit Card",
value: "credit_card",
description: "Pay with your credit or debit card",
},
{
label: "PayPal",
value: "paypal",
description: "Pay with your PayPal account",
},
]}
isRequired
/>Three upload styles: default, avatar, button.
<Field.Upload
name="documents"
label="Documents"
accept=".pdf,.doc,.docx"
multiple
maxFiles={3}
maxSize={10 * 1024 * 1024} // 10MB per file
showPreview
description="Upload up to 3 documents"
/><Field.Upload
name="avatar"
uploadStyle="avatar"
label="Profile Picture"
accept="image/*"
maxSize={5 * 1024 * 1024} // 5MB
showPreview
/><Field.Upload
name="resume"
uploadStyle="button"
label="Resume"
accept=".pdf"
maxSize={2 * 1024 * 1024} // 2MB
/><Field.Upload
name="avatar"
label="Profile Picture"
accept="image/*"
showPreview
onChangeSideEffect={(files) => {
if (files && files.length > 0) {
console.log("File selected:", files[0].name);
// Upload to server, show toast, etc.
}
}}
/>By default, inputs use floating labels. When you supply a placeholder, the label automatically floats to show the placeholder.
// Floating label (default)
<Field.Input
name="email"
label="Email"
placeholder="Enter your email" // Label auto-floats
/>
// Static label (above input)
<Field.Input
name="email"
label="Email"
labelMode="static"
/>Label behavior:
- No placeholder: Label floats on focus or when field has value
- With placeholder: Label always floats (to show placeholder)
- OTP inputs: Always use static label
- Static mode: Label always above input
Execute code when field value changes (without interfering with form state):
<Field.Input
name="email"
type="email"
label="Email"
onChangeSideEffect={(value) => {
// Check email availability
checkEmailAvailability(value);
}}
/>
<Field.Select
name="country"
label="Country"
options={[...]}
onChangeSideEffect={(value) => {
// Load states based on country
loadStates(value);
}}
/>
<Field.Upload
name="avatar"
label="Avatar"
onChangeSideEffect={(files) => {
// Upload to server immediately
if (files) uploadToServer(files[0]);
}}
/>Use React Hook Form's validation:
const form = useForm<FormData>({
defaultValues: { email: "" },
});
// In your form:
<Field.Input
name="email"
type="email"
label="Email"
required // Basic HTML5 validation
/>;For advanced validation, use a schema library (Zod, Yup):
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
age: z.number().min(18, "Must be 18 or older"),
});
const form = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: "", password: "", age: 18 },
});Error messages automatically appear below the field.
All components support light and dark modes out of the box.
// Uses system theme by default
<Field.Input name="email" label="Email" />// Force light mode for this field
<Field.Input
name="email"
label="Email"
theme="light"
/>
// Force dark mode for this field
<Field.Input
name="email"
label="Email"
theme="dark"
/>Add a theme toggle button:
import { ThemeToggle } from "@/components/ThemeToggle";
function MyPage() {
return (
<div>
<ThemeToggle />
{/* Your content */}
</div>
);
}The components use these Tailwind colors:
Light Mode:
- Background:
bg-white - Text:
text-gray-900 - Border:
border-gray-200 - Focus:
ring-gray-900
Dark Mode:
- Background:
bg-gray-950 - Text:
text-gray-100 - Border:
border-gray-800 - Focus:
ring-gray-100
To customize, modify the getThemeClasses function in useThemeMode.ts or update Tailwind config.
// ✅ Good
interface FormData {
email: string;
password: string;
}
const form = useForm<FormData>({ defaultValues: { email: "", password: "" } });
// ❌ Bad
const form = useForm(); // No type safety// ✅ Good
const form = useForm<FormData>({
defaultValues: {
email: "",
preferences: [],
acceptTerms: false,
},
});
// ❌ Bad
const form = useForm<FormData>(); // Fields start as undefined// ✅ Good
<Field.Input
name="password"
type="password"
label="Password"
helperText="Must be at least 8 characters with a number and special character"
/>
// ❌ Less helpful
<Field.Input
name="password"
type="password"
label="Password"
/><section className="space-y-4">
<h2>Personal Information</h2>
<div className="grid grid-cols-2 gap-4">
<Field.Input name="firstName" label="First Name" />
<Field.Input name="lastName" label="Last Name" />
</div>
<Field.Input name="email" type="email" label="Email" />
</section>const onSubmit = async (data: FormData) => {
try {
await api.submit(data);
toast.success("Form submitted successfully!");
} catch (error) {
toast.error("Failed to submit form");
}
};// ✅ Good - non-blocking operations
<Field.Input
name="username"
onChangeSideEffect={async (value) => {
await checkUsernameAvailability(value);
}}
/>
// ❌ Bad - blocking operations
<Field.Input
name="username"
onChangeSideEffect={(value) => {
form.setValue("email", value + "@example.com"); // Don't modify form state here
}}
/>| Prop | Type | Default | Description |
|---|---|---|---|
name |
Path<T> |
Required | Field name matching form schema |
label |
string |
- | Label text displayed above/around field |
description |
string |
- | Description text displayed below field |
isRequired |
boolean |
false |
Whether field is required |
isDisabled |
boolean |
false |
Whether field is disabled |
isReadOnly |
boolean |
false |
Whether field is read-only |
className |
string |
- | Additional CSS classes |
| Prop | Type | Default | Description |
|---|---|---|---|
type |
string |
"text" |
HTML input type |
placeholder |
string |
- | Placeholder text |
variant |
HeroUI Variant | - | Input style variant |
radius |
HeroUI Radius | - | Border radius |
size |
HeroUI Size | - | Input size |
labelPlacement |
HeroUI LabelPlacement | - | Label positioning |
onChangeSideEffect |
(value: any) => void |
- | Side effect callback |
startContent |
ReactNode |
- | Icon/content at start of input |
endContent |
ReactNode |
- | Icon/content at end of input |
isClearable |
boolean |
false |
Show clear button |
errorMessage |
string |
- | Custom error message (from RHF) |
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder |
string |
- | Placeholder text |
minRows |
number |
3 |
Minimum rows |
maxRows |
number |
- | Maximum rows (auto-expands) |
variant |
HeroUI Variant | - | Textarea style variant |
radius |
HeroUI Radius | - | Border radius |
size |
HeroUI Size | - | Textarea size |
onChangeSideEffect |
(value: any) => void |
- | Side effect callback |
| Prop | Type | Default | Description |
|---|---|---|---|
length |
number |
6 |
Number of OTP digits |
variant |
HeroUI Variant | - | OTP input style variant |
onChangeSideEffect |
(value: any) => void |
- | Side effect callback |
| Prop | Type | Default | Description |
|---|---|---|---|
options |
SelectOption[] |
Required | Array of options |
sections |
SelectSection[] |
- | Grouped sections with options |
placeholder |
string |
- | Placeholder text |
selectionMode |
"single" | "multiple" |
"single" |
Single or multi-select mode |
disabledKeys |
string[] |
- | Array of disabled option keys |
isLoading |
boolean |
false |
Show loading spinner |
isVirtualized |
boolean |
false |
Enable virtual scrolling (large lists) |
variant |
HeroUI Variant | - | Select style variant |
radius |
HeroUI Radius | - | Border radius |
size |
HeroUI Size | - | Select size |
onChangeSideEffect |
(value: any) => void |
- | Side effect callback |
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder |
string |
- | Placeholder text |
variant |
HeroUI Variant | - | DatePicker style variant |
radius |
HeroUI Radius | - | Border radius |
size |
HeroUI Size | - | DatePicker size |
onChangeSideEffect |
(value: any) => void |
- | Side effect callback |
| Prop | Type | Default | Description |
|---|---|---|---|
onChangeSideEffect |
(value: boolean) => void |
- | Side effect callback |
| Prop | Type | Default | Description |
|---|---|---|---|
options |
CheckboxOption[] |
Required | Array of checkbox options |
onChangeSideEffect |
(values: any[]) => void |
- | Side effect callback |
| Prop | Type | Default | Description |
|---|---|---|---|
options |
CheckboxOption[] |
Required | Array of checkbox options |
placeholder |
string |
"Search..." |
Search input placeholder |
maxHeight |
string |
"300px" |
Max height of scrollable list |
onChangeSideEffect |
(values: any[]) => void |
- | Side effect callback |
| Prop | Type | Default | Description |
|---|---|---|---|
options |
RadioOption[] |
Required | Array of radio options |
horizontal |
boolean |
false |
Display options horizontally |
onChangeSideEffect |
(value: any) => void |
- | Side effect callback |
| Prop | Type | Default | Description |
|---|---|---|---|
uploadStyle |
"default" | "avatar" | "button" |
"default" |
Upload UI style |
accept |
string |
- | Accepted file types |
multiple |
boolean |
false |
Allow multiple files |
maxFiles |
number |
- | Max number of files |
maxSize |
number |
- | Max file size in bytes |
showPreview |
boolean |
false |
Show file preview |
onChangeSideEffect |
(files: FileList | null) => void |
- | Side effect callback |
import { useForm } from "react-hook-form";
import { Form, Field } from "@/components/form";
interface LoginFormData {
email: string;
password: string;
rememberMe: boolean;
}
export function LoginForm() {
const form = useForm<LoginFormData>({
defaultValues: {
email: "",
password: "",
rememberMe: false,
},
});
const onSubmit = async (data: LoginFormData) => {
try {
await api.login(data);
toast.success("Logged in successfully!");
} catch (error) {
toast.error("Login failed");
}
};
return (
<Form form={form} onSubmit={onSubmit} className="space-y-4">
<Field.Input
name="email"
type="email"
label="Email"
placeholder="you@example.com"
isRequired
/>
<Field.Input
name="password"
type="password"
label="Password"
placeholder="Enter password"
isRequired
/>
<Field.Checkbox name="rememberMe" label="Remember me" />
<button
type="submit"
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Login
</button>
</Form>
);
}interface ProfileFormData {
firstName: string;
lastName: string;
bio: string;
country: string;
avatar: FileList | null;
newsletter: boolean;
}
export function ProfileForm() {
const form = useForm<ProfileFormData>({
defaultValues: {
firstName: "",
lastName: "",
bio: "",
country: "",
avatar: null,
newsletter: false,
},
});
const onSubmit = async (data: ProfileFormData) => {
// Handle form submission
};
return (
<Form form={form} onSubmit={onSubmit} className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<Field.Input
name="firstName"
label="First Name"
placeholder="John"
isRequired
/>
<Field.Input
name="lastName"
label="Last Name"
placeholder="Doe"
isRequired
/>
</div>
<Field.Textarea
name="bio"
label="Bio"
minRows={4}
placeholder="Tell us about yourself..."
/>
<Field.Select
name="country"
label="Country"
options={[
{ key: "us", label: "United States", value: "us" },
{ key: "uk", label: "United Kingdom", value: "uk" },
{ key: "ca", label: "Canada", value: "ca" },
]}
placeholder="Select country"
isRequired
/>
<Field.Upload
name="avatar"
uploadStyle="avatar"
label="Profile Picture"
accept="image/*"
maxSize={5 * 1024 * 1024}
showPreview
/>
<Field.Checkbox name="newsletter" label="Subscribe to newsletter" />
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
>
Update Profile
</button>
</Form>
);
}import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const registrationSchema = z
.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string(),
acceptTerms: z
.boolean()
.refine((val) => val === true, "You must accept the terms"),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
type RegistrationFormData = z.infer<typeof registrationSchema>;
export function RegistrationForm() {
const form = useForm<RegistrationFormData>({
resolver: zodResolver(registrationSchema),
defaultValues: {
email: "",
password: "",
confirmPassword: "",
acceptTerms: false,
},
});
const onSubmit = async (data: RegistrationFormData) => {
await api.register(data);
};
return (
<Form form={form} onSubmit={onSubmit} className="space-y-4">
<Field.Input
name="email"
type="email"
label="Email"
placeholder="you@example.com"
required
/>
<Field.Input
name="password"
type="password"
label="Password"
placeholder="Create a password"
required
/>
<Field.Input
name="confirmPassword"
type="password"
label="Confirm Password"
placeholder="Confirm your password"
required
/>
<Field.Checkbox
name="acceptTerms"
label="I accept the terms and conditions"
required
/>
<button
type="submit"
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg"
>
Register
</button>
</Form>
);
}import { useState } from "react";
interface MultiStepFormData {
// Step 1
firstName: string;
lastName: string;
email: string;
// Step 2
country: string;
phone: string;
// Step 3
preferences: string[];
newsletter: boolean;
}
export function MultiStepForm() {
const [step, setStep] = useState(1);
const form = useForm<MultiStepFormData>({
defaultValues: {
firstName: "",
lastName: "",
email: "",
country: "",
phone: "",
preferences: [],
newsletter: false,
},
});
const onSubmit = async (data: MultiStepFormData) => {
await api.submit(data);
};
return (
<Form form={form} onSubmit={onSubmit} className="space-y-6">
{step === 1 && (
<>
<h2>Personal Information</h2>
<Field.Input name="firstName" label="First Name" isRequired />
<Field.Input name="lastName" label="Last Name" isRequired />
<Field.Input name="email" type="email" label="Email" isRequired />
<button type="button" onClick={() => setStep(2)}>Next</button>
</>
)}
{step === 2 && (
<>
<h2>Contact Information</h2>
<Field.Select
name="country"
label="Country"
options={[...]}
isRequired
/>
<Field.Input name="phone" type="tel" label="Phone" />
<button type="button" onClick={() => setStep(1)}>Back</button>
<button type="button" onClick={() => setStep(3)}>Next</button>
</>
)}
{step === 3 && (
<>
<h2>Preferences</h2>
<Field.CheckboxGroup
name="preferences"
label="Interests"
options={[...]}
/>
<Field.Checkbox name="newsletter" label="Subscribe to newsletter" />
<button type="button" onClick={() => setStep(2)}>Back</button>
<button type="submit">Submit</button>
</>
)}
</Form>
);
}Problem: Field values don't update when typing.
Solution: Make sure you wrapped your form fields in the <Form> component and passed the form instance:
const form = useForm<FormData>();
return (
<Form form={form} onSubmit={onSubmit}>
<Field.Input name="email" label="Email" />
</Form>
);Problem: TypeScript complains about field names.
Solution: Define your form data type and pass it to useForm:
interface FormData {
email: string;
}
const form = useForm<FormData>({ defaultValues: { email: "" } });Problem: Components don't change appearance in dark mode.
Solution: Make sure you:
- Wrapped your app with
ThemeProvider - Added
darkMode: 'class'totailwind.config.js - Added
suppressHydrationWarningto the<html>tag
Problem: Can't type in OTP boxes.
Solution: Make sure the field value is initialized as an empty string:
const form = useForm<FormData>({
defaultValues: {
verificationCode: "", // ← Initialize as empty string, not undefined
},
});Before:
const [email, setEmail] = useState("");
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Email"
/>;After:
const form = useForm<{ email: string }>({
defaultValues: { email: "" },
});
<Form form={form} onSubmit={onSubmit}>
<Field.Input name="email" type="email" label="Email" placeholder="Email" />
</Form>;Before:
<TextField
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={!!error}
helperText={error}
required
/>After:
<Field.Input
name="email"
type="email"
label="Email"
required
helperText="Enter your email address"
/>For issues, questions, or feature requests, please refer to:
- React Hook Form docs: https://react-hook-form.com/
- Next.js docs: https://nextjs.org/docs
- Tailwind CSS docs: https://tailwindcss.com/docs
This form system is part of your project and follows your project's license.
Last Updated: November 20, 2025 Version: 1.0.0