How to Use Shadcn UI in React

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 --eslint

Once the setup is finished, navigate into your project folder:

cd real-estate-dashboard

Step 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 init

The 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 button

This 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.

Shadcn UI in React

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 label

Step 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.

Use Shadcn UI in React

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 table

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

  1. Keep UI Pure: Don’t put business logic inside the components/ui folder.
  2. Use Variants: Leverage the class-variance-authority (CVA) to create reusable styles.
  3. 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:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.