I have been building React applications for over the years now. During this time, I’ve used almost every UI library under the sun.
From the early days of Bootstrap to the massive Material UI ecosystem. But recently, something changed the way I think about components.
That something is Shadcn UI. It isn’t just another library you install and forget. It’s a collection of reusable components that you actually own.
I’ve found that this “copy-paste” approach solves a huge problem. You no longer have to fight against a library’s rigid CSS or bloated bundle sizes.
In this guide, I will show you how I use Shadcn UI in my daily projects. I’ll share the exact steps I take to set up a professional-grade UI.
What is Shadcn UI?
Shadcn UI is not a library in the traditional sense. It’s a set of components built on top of Radix UI and Tailwind CSS.
Instead of installing it via npm, you add individual files to your project. This gives you full control over every single line of code in your UI.
It handles the hard parts like accessibility and keyboard navigation for you. You just focus on making it look exactly how you want.
How to Install Shadcn UI in a React Project
Setting up Shadcn UI is a bit different than other libraries. I usually start with a fresh Vite or Next.js project.
For this example, let’s assume we are building a “Real Estate Dashboard” for a US-based firm.
Step 1: Initialize Your React Project
First, you need a project with Tailwind CSS already configured.
Open your terminal and run the following command:
npx create-next-app@latest real-estate-dashboard --typescript --tailwind --eslintOnce the setup is finished, navigate into your project folder:
cd real-estate-dashboardStep 2: Initialize Shadcn UI CLI
Now, we use the Shadcn CLI to set up our project configuration. I love this tool because it automates the tedious CSS variable setup.
Run this command in your terminal:
npx shadcn@latest initThe CLI will ask you a few questions. Here is how I typically answer them:
- Style: Default
- Base Color: Slate
- CSS Variables: Yes
This creates a components.json file and sets up your tailwind.config.js.
Method 1: Add and Customize a Button Component
The most common task is adding a simple component like a Button.
In a professional dashboard, you might need a “Schedule Tour” button.
Step 1: Add the Button Component
Use the CLI to pull the Button code into your local directory:
npx shadcn@latest add buttonThis creates a file at components/ui/button.tsx.
Step 2: Implementation Code
Here is how I would use this button in a property listing card:
import { Button } from "@/components/ui/button"
import { CalendarDays } from "lucide-react"
export default function PropertyAction() {
const handleSchedule = () => {
alert("Redirecting to our NYC office booking system...");
};
return (
<div className="p-6 border rounded-lg shadow-sm bg-white max-w-sm">
<h2 className="text-xl font-bold mb-2">Luxury Loft in Soho</h2>
<p className="text-gray-600 mb-4">$4,500 / month</p>
{/* Using the Shadcn Button with an icon */}
<Button
onClick={handleSchedule}
className="w-full bg-blue-700 hover:bg-blue-800 text-white"
>
<CalendarDays className="mr-2 h-4 w-4" />
Schedule a Tour
</Button>
</div>
)
}I executed the above example code and added the screenshot below.

I prefer this because I can easily tweak the button.tsx file if I need a custom “New York” style border.
Method 2: Create Complex Forms with Shadcn and Zod
Forms are where Shadcn UI really shines for me. It integrates perfectly with react-hook-form and zod for validation.
Let’s build a “Mortgage Lead” form for a California-based lending site.
Step 1: Add Necessary Components
We need the Form, Input, and Label components:
npx shadcn@latest add form input labelStep 2: Complete Form Code
I always use TypeScript to ensure the lead data is valid before submission.
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
// Define validation schema for a US Mortgage Lead
const formSchema = z.object({
fullName: z.string().min(2, {
message: "Name must be at least 2 characters.",
}),
zipCode: z.string().regex(/^\d{5}$/, {
message: "Please enter a valid 5-digit US Zip Code.",
}),
annualIncome: z.coerce.number().min(30000, {
message: "Minimum eligible income is $30,000.",
}),
})
export function MortgageLeadForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
fullName: "",
zipCode: "",
annualIncome: 50000,
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log("Submitting to US Lending API:", values)
}
return (
<div className="max-w-md mx-auto p-8 border rounded-xl bg-slate-50">
<h3 className="text-2xl font-semibold mb-6">Prequalify for a Loan</h3>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="fullName"
render={({ field }) => (
<FormItem>
<FormLabel>Full Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="zipCode"
render={({ field }) => (
<FormItem>
<FormLabel>Zip Code</FormLabel>
<FormControl>
<Input placeholder="90210" {...field} />
</FormControl>
<FormDescription>We use this to find local rates.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="annualIncome"
render={({ field }) => (
<FormItem>
<FormLabel>Annual Household Income ($)</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Check My Rate</Button>
</form>
</Form>
</div>
)
}I executed the above example code and added the screenshot below.

This setup ensures that users in Los Angeles or Chicago can’t submit invalid data.
Method 3: Use Data Tables for Analytics
If you are building an admin panel, you will definitely need a Data Table. Shadcn uses TanStack Table under the hood, which is incredibly powerful.
Let’s create a table to track “Monthly Sales Tax” across different US states.
Step 1: Install Data Table Components
This requires a bit more setup since it’s a complex component:
npx shadcn@latest add tableStep 2: Implementation Code
I find that keeping the columns in a separate array makes the code much cleaner.
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const taxData = [
{ state: "California", rate: "7.25%", status: "Active" },
{ state: "Texas", rate: "6.25%", status: "Active" },
{ state: "Florida", rate: "6.00%", status: "Active" },
{ state: "New York", rate: "4.00%", status: "Review Required" },
{ state: "Nevada", rate: "6.85%", status: "Active" },
]
export function SalesTaxTable() {
return (
<div className="rounded-md border m-10">
<Table>
<TableCaption>A list of state-level sales tax rates for 2024.</TableCaption>
<TableHeader className="bg-slate-100">
<TableRow>
<TableHead className="w-[150px]">US State</TableHead>
<TableHead>Standard Rate</TableHead>
<TableHead>Compliance Status</TableHead>
<TableHead className="text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{taxData.map((item) => (
<TableRow key={item.state}>
<TableCell className="font-medium">{item.state}</TableCell>
<TableCell>{item.rate}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded-full text-xs ${
item.status === 'Active' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{item.status}
</span>
</TableCell>
<TableCell className="text-right">
<button className="text-blue-600 hover:underline">Edit</button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}This table is responsive and accessible right out of the box.
Why I Prefer Shadcn UI Over Material UI
In my experience, Material UI often leads to “Style Hell.”
You spend hours trying to override a nested MuiButton-root class.
With Shadcn, you just open button.tsx and change the Tailwind classes.
It’s predictable, fast, and stays out of your way.
Plus, the bundle size is tiny because you only ship the code you actually use.
Best Practices for Professional Projects
When I build apps for clients in the US, I follow these rules:
- Keep UI Pure: Don’t put business logic inside the components/ui folder.
- Use Variants: Leverage the class-variance-authority (CVA) to create reusable styles.
- Theme Early: Set up your primary colors in globals.css before adding components.
This keeps the codebase maintainable as the project grows. Shadcn UI has completely changed how I approach React development.
It gives me the speed of a library with the flexibility of custom code. I hope this tutorial helps you build your next big project faster.
You may read:
- Parent-Child Component Communication in React
- React Render Component on Button Click
- Ways to Display JSON in React
- Implement Scroll to Component in React

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.