For web developers, the quest for the perfect UI toolkit is a constant endeavor. For years, React developers have relied on traditional component libraries like Material-UI (MUI), Ant Design, and Chakra UI. These libraries offer a wealth of pre-built components, promising to accelerate development. However, they often come with a trade-off: a lack of control, style overrides that feel like a battle, and bloated bundle sizes.
Enter Shadcn UI, a paradigm-shifting approach that has taken the React community by storm. It's not a component library in the way you're used to; it's something better. It's a collection of beautifully designed, accessible, and endlessly reusable components that you don't install from npm as a dependency—you copy them directly into your project.
This comprehensive, 4000-word tutorial will serve as your definitive guide, taking you from a complete beginner to a confident Shadcn UI practitioner. We will explore its foundational philosophy, walk through a detailed setup, build complex UIs, master advanced theming and form handling, and discuss best practices for large-scale applications. Prepare to rethink what you expect from a UI toolkit.
Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?
Apidog delivers all your demands, and replaces Postman at a much more affordable price!
The Shadcn UI Philosophy - A New Way of Building
Before writing a single line of code, it's paramount to understand why Shadcn UI exists and what problems it solves. Grasping this core philosophy is the key to unlocking its full potential.
What Shadcn UI Is Not
- It is not a traditional npm package. You won't find
shadcn-ui
in yourpackage.json
's dependencies list. This is the most crucial distinction. - It is not a monolithic library. It doesn't force you to install hundreds of components when you only need a button and an input field.
- It is not restrictive. You are never locked into a specific design aesthetic or limited by the theming capabilities provided by a library's maintainers.
What Shadcn UI Is
- A Collection of Reusable Code: Think of it as an expertly curated set of recipes. You pick the recipe you want (e.g., a
Card
component), and the instructions (the code) are given to you to cook in your own kitchen (your project). - A Commitment to Code Ownership: Once you add a Shadcn component, its source code—a
.tsx
file—is placed directly into your codebase, typically undercomponents/ui/
. It is now your component. You can change its structure, its styles, its logic—anything. This eliminates the frustrating experience of wrestling with!important
CSS overrides or complex prop APIs to achieve a simple visual tweak. - Built on a Modern, Solid Foundation: Shadcn UI doesn't reinvent the wheel. It stands on the shoulders of giants:
- Tailwind CSS: A utility-first CSS framework that provides low-level building blocks for creating any design directly in your markup. Shadcn components are styled exclusively with Tailwind, making them incredibly easy to customize if you're familiar with the framework.
- Radix UI: A library of unstyled, accessible, low-level UI primitives. Radix handles all the complex and often-overlooked aspects of UI components, such as keyboard navigation, focus management, and ARIA attributes for accessibility (a11y). Shadcn takes these powerful, headless primitives and adds beautiful styling with Tailwind CSS.
The primary advantage of this model is the fusion of speed and control. You get the initial velocity of using pre-built components without sacrificing the long-term flexibility and maintainability that comes from owning your own code.
Setting the Stage - Project Setup and Installation
Let's transition from theory to practice. We'll set up a new project from scratch. For this guide, we'll primarily use Next.js, as its server components and file-based routing align perfectly with the Shadcn UI ethos. We'll also briefly cover setup for Vite.
Step 1: Environment Prerequisites
Ensure your development environment is ready. You will need:
- Node.js: The latest Long-Term Support (LTS) version is recommended. You can download it from the official Node.js website.
- A Package Manager: This tutorial will use
npm
, which is bundled with Node.js. You can also useyarn
orpnpm
.
Step 2: Creating a New Next.js Application
Open your terminal and execute the following command to bootstrap a new Next.js project.Bash
npx create-next-app@latest my-pro-shadcn-app --typescript --tailwind --eslint
This command scaffolds a new application in a directory named my-pro-shadcn-app
. We've included some important flags:
--typescript
: Shadcn UI is written in TypeScript and works best in a TypeScript environment.--tailwind
: Tailwind CSS is a hard dependency for Shadcn UI's styling.--eslint
: Always a good practice for maintaining code quality.
The installer will ask you a few questions. These are the recommended choices for a modern Next.js 14+ setup:
✔ Would you like to use `src/` directory? … No / **Yes**
✔ Would you like to use App Router? (recommended) … No / **Yes**
✔ Would you like to customize the default import alias? … **No** / Yes
Using the App Router is standard practice, and the src/
directory helps in organizing code. Once done, navigate into your new project:Bash
cd my-pro-shadcn-app
Step 3: The init
Command - Bringing Shadcn UI to Life
This is the most important step. Shadcn UI provides a CLI tool to configure your project. Run the following command from your project's root directory:Bash
npx shadcn-ui@latest init
This will trigger an interactive questionnaire to set up your project. Let's break down each question and its significance:
- Would you like to use TypeScript (recommended)?
Yes
. We are in a TypeScript project. - Which style would you like to use?
Default
vs.New York
. These are two pre-defined visual styles.Default
is a bit more spacious, whileNew York
is more compact. You can see examples on the Shadcn UI website. Let's chooseDefault
. - Which color would you like to use as a base color? This sets up the primary color palette for your UI. The default is
Slate
. Let's stick withSlate
for now; we'll learn how to change this later. - Where is your
global.css
file? The CLI correctly detects this atsrc/app/globals.css
. This file is where the core CSS variables for theming will be injected. - Do you want to use CSS variables for theming?
Yes
. This is the cornerstone of Shadcn's theming system, allowing for dynamic changes (like dark/light mode) and easy customization. - Where is your
tailwind.config.ts
located? The CLI detectssrc/tailwind.config.ts
. This file will be modified to integrate Shadcn's theme presets. - Configure import alias for components:
@/components
. This is a best practice. It means that no matter how deeply nested a file is, you can always import a component with a clean path likeimport { Button } from "@/components/ui/button";
. - Configure import alias for utils:
@/lib/utils
. Same as above, for utility functions. - Are you using React Server Components?
Yes
. We chose the App Router, which uses Server Components by default. - Write configuration to
components.json
?Yes
. This creates a crucial file that remembers all your choices, so you don't have to answer these questions every time you runnpx shadcn-ui@latest add ...
.
After you confirm, the CLI works its magic:
- Installs Dependencies: It adds necessary packages like
tailwindcss-animate
andclass-variance-authority
. - Creates
components.json
: Stores your configuration choices. - Updates
tailwind.config.ts
: Injects the Shadcn UI plugin and theming configuration. - Updates
globals.css
: Adds a large block of CSS variables that define your entire color palette, border radii, and more. - Creates
lib/utils.ts
: This file exports acn
helper function, which is a clever utility for conditionally merging Tailwind CSS classes.
Your project is now fully configured.
(Alternative: Vite Setup)
If you're using Vite with React, the process is very similar. After setting up a Vite + React + TS project, you would manually install Tailwind CSS and then run npx shadcn-ui@latest init. The CLI is smart enough to detect a Vite setup and will ask slightly different questions about file locations (e.g., index.css instead of globals.css).
Building a UI - From Simple Components to Complex Layouts
With setup complete, let's start building. The core workflow is: identify a need, add the component, use it.
Step 4: Adding and Using Your First Components
Let's clean up the default Next.js boilerplate and build a simple interface.
1. Add a Button:Bash
npx shadcn-ui@latest add button
Observe what happens: a new file, src/components/ui/button.tsx
, is created. This is your button. You own it.
2. Add a Card:Bash
npx shadcn-ui@latest add card
This command is more interesting. It creates src/components/ui/card.tsx
. If you inspect this file, you'll see it exports multiple components: Card
, CardHeader
, CardTitle
, CardDescription
, CardContent
, and CardFooter
. This is a common pattern for compound components.
3. Build the UI:
Now, open src/app/page.tsx
and replace its content with the following:TypeScript
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input"; // We'll add this next
import { Label } from "@/components/ui/label"; // And this
export default function Home() {
return (
<main className="flex min-h-screen items-center justify-center bg-background p-8">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl">Create Project</CardTitle>
<CardDescription>
Deploy your new project in one-click.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Name of your project" />
</div>
<div className="grid gap-2">
<Label htmlFor="framework">Framework</Label>
{/* We'll replace this with a Select component later */}
<Input id="framework" placeholder="e.g. Next.js" />
</div>
</CardContent>
<CardFooter>
<Button className="w-full">Deploy</Button>
</CardFooter>
</Card>
</main>
);
}
Our code won't run yet because we're missing the Input
and Label
components. Let's add them:Bash
npx shadcn-ui@latest add input
npx shadcn-ui@latest add label
Now, run your development server:Bash
npm run dev
Navigate to http://localhost:3000
. You'll see a clean, professional-looking form within a card. Notice how we used utility classes like w-full
, max-w-md
, and grid
directly in our JSX to control the layout. This is the power of combining Shadcn and Tailwind CSS.
Step 5: Introducing More Sophisticated Components
Static inputs are good, but real apps need interactive elements. Let's enhance our form.
1. Add a Select
Component: The "Framework" input should be a dropdown. Let's add the Select
component. This one is more complex and has dependencies on other components.Bash
npx shadcn-ui@latest add select
The CLI is smart. It will see that Select
requires a Popover
component to function and will ask for your permission to install it and its dependencies as well. This is a fantastic feature that prevents you from having to manually track dependencies.
2. Integrate the Select
Component: Replace the Input
for "Framework" in src/app/page.tsx
with the new Select
component.TypeScript
// Add these imports at the top
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// ... inside the CardContent
<div className="grid gap-2">
<Label htmlFor="framework">Framework</Label>
<Select>
<SelectTrigger id="framework">
<SelectValue placeholder="Select a framework" />
</SelectTrigger>
<SelectContent>
<SelectItem value="nextjs">Next.js</SelectItem>
<SelectItem value="sveltekit">SvelteKit</SelectItem>
<SelectItem value="astro">Astro</SelectItem>
<SelectItem value="nuxt">Nuxt.js</SelectItem>
</SelectContent>
</Select>
</div>
Refresh your browser. You now have a fully functional and accessible select dropdown, complete with animations and proper keyboard navigation, all thanks to Radix UI working under the hood.
3. Adding User Feedback with Toast
: What happens when a user clicks "Deploy"? We should give them some feedback. The Toast
component is perfect for this.
First, add it:Bash
npx shadcn-ui@latest add toast
Next, to use toasts, you need to add a <Toaster />
component to your root layout so it can be displayed anywhere in the app. Open src/app/layout.tsx
and modify it:TypeScript
import { Toaster } from "@/components/ui/toaster" // Import the Toaster
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Toaster /> {/* Add it here, just before closing body */}
</body>
</html>
)
}
Now, we need a way to trigger the toast. We'll use the useToast
hook. Let's update src/app/page.tsx
to make it a client component and handle the button click.TypeScript
'use client'; // <-- Add this at the very top of the file
// ... other imports
import { useToast } from "@/components/ui/use-toast";
export default function Home() {
const { toast } = useToast(); // Get the toast function from the hook
function handleDeploy() {
toast({
title: "Deployment Scheduled!",
description: "Your project 'Name of your project' is being deployed.",
duration: 5000,
});
}
return (
<main className="flex min-h-screen items-center justify-center bg-background p-8">
<Card className="w-full max-w-md">
{/* ... CardHeader and CardContent ... */}
<CardFooter>
<Button className="w-full" onClick={handleDeploy}> {/* Add onClick handler */}
Deploy
</Button>
</CardFooter>
</Card>
</main>
);
}
Now, when you click the "Deploy" button, a sleek notification will appear at the corner of your screen.
Building a Professional Form with Validation
Most real-world applications require robust form handling, including client-side validation. The official way to handle this with Shadcn UI is by combining it with react-hook-form
for state management and zod
for schema validation. Let's build it.
Step 6: Installing Form Dependencies
First, let's install the necessary libraries:Bash
npm install react-hook-form zod @hookform/resolvers
react-hook-form
: A performant, flexible, and extensible forms library.zod
: A TypeScript-first schema declaration and validation library.@hookform/resolvers
: A bridge library to allowreact-hook-form
to usezod
for validation.
Step 7: Adding the Shadcn Form
Component
Shadcn UI provides a special Form
component that acts as a wrapper to seamlessly connect react-hook-form
with your UI components.Bash
npx shadcn-ui@latest add form
This will add src/components/ui/form.tsx
. This file provides a set of context-aware components (Form
, FormField
, FormItem
, FormLabel
, FormControl
, FormDescription
, FormMessage
) that drastically reduce boilerplate.
Step 8: Creating the Validation Schema
In your src/app/page.tsx
, let's define the shape and rules of our form data using zod
.TypeScript
// Add these imports at the top
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
Now, let's create the schema just above our Home
component:TypeScript
const formSchema = z.object({
projectName: z.string().min(2, {
message: "Project name must be at least 2 characters.",
}).max(50, {
message: "Project name must not exceed 50 characters.",
}),
framework: z.string({
required_error: "Please select a framework to display.",
}),
});
This schema defines two fields: projectName
must be a string between 2 and 50 characters, and framework
is a required string.
Step 9: Wiring Up the Form
Now, let's refactor our Home
component to use all these new tools.TypeScript
export default function Home() {
const { toast } = useToast();
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
projectName: "",
},
});
// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values);
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
</pre>
),
});
}
// 3. Build the JSX with Shadcn's Form components
return (
<main className="flex min-h-screen items-center justify-center bg-background p-8">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl">Create Project</CardTitle>
<CardDescription>
Deploy your new project in one-click.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Name of your project" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage /> {/* Displays validation errors */}
</FormItem>
)}
/>
<FormField
control={form.control}
name="framework"
render={({ field }) => (
<FormItem>
<FormLabel>Framework</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a framework" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="nextjs">Next.js</SelectItem>
<SelectItem value="sveltekit">SvelteKit</SelectItem>
<SelectItem value="astro">Astro</SelectItem>
<SelectItem value="nuxt">Nuxt.js</SelectItem>
</SelectContent>
</Select>
<FormDescription>
The framework you want to deploy.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Deploy</Button>
</form>
</Form>
</CardContent>
</Card>
</main>
);
}
This is a significant chunk of code, but it's an incredibly powerful and scalable pattern. The FormField
component handles all the state connections, and FormMessage
automatically displays the correct validation error from your zod
schema when a user interacts with the field. Try submitting the form with an empty project name to see the validation in action.
Mastering Theming and Customization
The true power of Shadcn UI is unleashed when you start making it your own.
Step 10: Advanced Theming with CSS Variables
Your entire theme is defined by CSS variables in src/app/globals.css
. Open this file and look for the :root
and .dark
blocks.CSS
/* Example from globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* ... and many more */
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... */
}
- Changing Colors: The values are represented as HSL (Hue, Saturation, Lightness) values without the
hsl()
wrapper. This is a deliberate choice for easier manipulation. To change your primary brand color, you just need to find the HSL values for your color and update the--primary
and--primary-foreground
variables. The Shadcn UI Themes page has a fantastic generator that lets you pick a color and copy-paste the entire theme block. - Changing Border Radius: Want sharper corners? Change
--radius: 0.5rem;
to--radius: 0.2rem;
or even0rem
. Every component that has rounded corners uses this variable, so your change will propagate globally.
Implementing Dark Mode:
Shadcn is pre-configured for dark mode thanks to the .dark class block and Tailwind's darkMode: "class" strategy in tailwind.config.ts. All you need is a way to toggle the dark class on the <html> element. A popular library for this is next-themes.
- Install it:
npm install next-themes
- Create a
ThemeProvider
component (src/components/theme-provider.tsx
): TypeScript
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
- Wrap your
RootLayout
in this provider (src/app/layout.tsx
): TypeScript
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
<Toaster />
</ThemeProvider>
</body>
</html>
)
}
- Finally, create a toggle button (e.g.,
src/components/mode-toggle.tsx
): TypeScript
"use client"
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ModeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="outline"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
You can now place this <ModeToggle />
anywhere in your app to get a system-aware, user-overrideable dark mode toggle.
Step 11: Customizing Component Source Code
This is the ultimate superpower. Let's say you want a new success variant for your button that has a green background.
Open src/components/ui/button.tsx. Find the buttonVariants definition. It uses cva (Class Variance Authority). Simply add a new variant:TypeScript
const buttonVariants = cva(
// ... base styles
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
success: "bg-green-600 text-white hover:bg-green-600/90", // Our new variant
},
// ... size variants
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
That's it. You can now use it in your code: <Button variant="success">Success</Button>
. You didn't need to write complex CSS overrides. You just edited the component's own source code. This workflow is simple, predictable, and incredibly powerful.
Part 6: Best Practices and The Road Ahead
As your application grows, here are some best practices to keep in mind.
- File Organization: While the CLI puts everything in
components/ui
, this folder should be considered your "base" UI kit. For more complex components that you compose yourself (e.g., aUserProfileCard
that uses Shadcn'sCard
,Avatar
, andButton
), create them in a different directory, likecomponents/shared
orcomponents/features
. This keeps a clear separation between foundational UI and application-specific components. - Component Updates: How do you get updates if the original Shadcn UI component is improved? The CLI has you covered. You can run
npx shadcn-ui@latest add button
again. The CLI will detect that you already have abutton.tsx
file and show you adiff
comparison, allowing you to either overwrite your file or accept the changes manually. It's like a mini version control for your components. - Leverage Accessibility: Remember that Shadcn components are accessible out of the box because they are built on Radix primitives. When you customize them, be mindful not to break this accessibility. For example, if you change a button's colors, ensure the text still has enough contrast. When you build new components, try to follow the patterns established by Shadcn/Radix to maintain keyboard navigability and screen reader support.
Conclusion: You Are the Library Author
You have now journeyed from the core philosophy of Shadcn UI to implementing advanced, real-world patterns. You've seen that its true innovation isn't just the components themselves, but the paradigm shift it represents. It moves developers from being mere consumers of a library to being curators and owners of their own UI toolkit.
By giving you the raw source code, building on the solid foundations of Tailwind CSS and Radix UI, and providing a seamless CLI experience, Shadcn UI strikes the perfect balance between initial development speed and long-term maintainability and creative freedom. You are no longer constrained by someone else's design system. The components in your project are your own—to modify, extend, and perfect.
The future of your application's UI is no longer in the hands of a third-party dependency; it's right there in your components
folder. Happy building.
Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?
Apidog delivers all your demands, and replaces Postman at a much more affordable price!