If you’ve been building stuff with Next.js, I’m pretty sure you’ve come across shadcn/ui. It’s kinda become the go-to component library for a lot of devs (myself included), and for good reason — the components look great and you can tweak them however you want.
But man, I’ve got a bone to pick with how they handle forms. Don’t get me wrong, it works fine when you’re just messing around with a small project. But once you start building something serious? That’s when the headaches begin.
Let me show you what I mean. Here’s what you need just to add ONE input field with their approach:
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
Like… seriously? All that for one input? 🤦♂️
This is fine when you’re throwing together a quick login form or something. But I’m currently working on an enterprise app with about 30 different forms, some with 20+ fields each. Using the standard shadcn approach would mean writing a novel’s worth of TSX.
Why This Drove Me Crazy
After copy-pasting similar form code for the hundredth time, I started noticing some really annoying problems:
Our codebase was getting bloated with basically the same form boilerplate everywhere.
Different devs on the team were handling error states differently
When design wanted to change how form fields looked, we had to update stuff in like 50 different places.I wasted an entire weekend refactoring a bunch of forms that should’ve taken me a few hours. That’s when I decided enough was enough.
The Better Way
So I spent some time thinking about how to fix this mess without throwing away shadcn (because the components themselves are actually great). What I came up with has saved me tons of time.
Here’s the basic idea — instead of writing out every form field manually, what if we just described what we wanted and let the code handle the rest?
How I Fixed It
First we need to create some from elements, you can add these anywhere i like to add them inside my components folder -> form-elements.
Here’s what my input element code looks like
import { useFormContext } from 'react-hook-form'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
interface InputElementProps extends React.InputHTMLAttributes<HTMLInputElement> {
name: string
label?: string
description?: string
isOptional?: boolean
inputClassName?: string
labelClassName?: string
}
const InputElement: React.FC<InputElementProps> = ({ name, label, placeholder, description, isOptional, labelClassName, inputClassName, ...props }) => {
const { control } = useFormContext();
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className={cn('', props.className)}>
{label && (
<FormLabel className={cn('', labelClassName)}>
{label}
{isOptional && (
<span className="text-neutral-400"> (optional)</span>
)}
</FormLabel>
)}
<FormControl>
<Input
{...field}
placeholder={placeholder}
className={cn('', inputClassName)}
type={props.type || 'text'}
disabled={props.disabled}
value={props.value || field.value}
autoComplete={props.autoComplete}
onChange={props.onChange}
/>
</FormControl>
{description && (
<FormDescription>
{description}
</FormDescription>
)}
<FormMessage />
</FormItem>
)}
/>
)
}
export default InputElement
Thats basically it, we now have all the input element attributes as props and we dont even have to pass the form states inside each element, we just need to wrap the forms inside the Form component from shadcn ui.
The Difference Is Huge
Check this out. Here’s what our login-form looks like now.
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { Form } from "@/components/ui/form"
import InputElement from "@/components/form-elements/input-element"
import { Button } from "@/components/ui/button"
import { LoginRequest, LoginSchema } from "@/zod-schemas/auth/login.schema"
import Link from "next/link"
import { PageRoutes } from "@/constants/page-routes"
const LoginForm = () => {
const form = useForm<LoginRequest>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: "",
password: "",
}
})
const onSubmit = async (values: LoginRequest) => {
console.log("Form submitted with values:", values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-4">
<InputElement
label="Email"
name="email"
placeholder="johndoe@gmail.com"
/>
<InputElement
label="Password"
name="password"
type="password"
placeholder="********"
/>
<Button variant="link" size="sm" className="px-0 h-fit" asChild>
<Link href={PageRoutes.AUTH.FORGOT_PASSWORD}>
Forgot Password?
</Link>
</Button>
</div>
<Button type="submit" className="w-full" size="lg">
Login
</Button>
</form>
</Form>
)
}
export default LoginForm
Way better, right?
Was It Worth It?
100%. What used to take me an entire day now takes about 10–20 minutes. Even the junior devs on our team can crank out complex forms without breaking a sweat.
The best part? When design wanted to update our form styles recently, I made the changes in ONE file instead of hunting down form fields across our entire app.
If you’re building anything beyond a tiny project with shadcn, trust me — spend a day setting up something like this. Your future self will thank you.
I’ve put together a github gist with all of the elements i use in my projects, if you wanna check it out here.
Let me know if this helps or if you’ve come up with an even better approach!
Connect with Me
If you enjoyed this post and want to stay in the loop with similar content, feel free to follow and connect with me across the web:
- Twitter: Follow me on Twitter for bite-sized tips, updates, and thoughts on tech.
- Medium: Check out my articles on Medium where I share tutorials, insights, and deep dives.
- Email: Got questions, ideas, or just want to say hi? Drop me a line at codezera3@gmail.com.
Your support means a lot, and I'd love to connect, collaborate, or just geek out over cool projects. Looking forward to hearing from you!
Top comments (1)
TBH, I thought it's basic, having our own custom component is a must, not even for input I used to create for model, slider, and lots of other components