What is Shadcn/UI? Beginner's Tutorial to Get Started

Audrey Lopez

Audrey Lopez

14 June 2025

What is Shadcn/UI? Beginner's Tutorial to Get Started

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 a great API Testing tool that generates beautiful API Documentation?

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

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

What Shadcn UI Is

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:

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:

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:

After you confirm, the CLI works its magic:

  1. Installs Dependencies: It adds necessary packages like tailwindcss-animate and class-variance-authority.
  2. Creates components.json: Stores your configuration choices.
  3. Updates tailwind.config.ts: Injects the Shadcn UI plugin and theming configuration.
  4. Updates globals.css: Adds a large block of CSS variables that define your entire color palette, border radii, and more.
  5. Creates lib/utils.ts: This file exports a cn 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

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%;
  /* ... */
}

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.

  1. Install it: npm install next-themes
  2. 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>
}
  1. 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>
  )
}
  1. 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.

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 a great API Testing tool that generates beautiful API Documentation?

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

Explore more

How to Create Apple's Liquid Glass Effects in React

How to Create Apple's Liquid Glass Effects in React

Apple has always been at the forefront of user interface design, and one of their most captivating recent effects is the "liquid glass" look. This effect, characterized by its fluid, jelly-like appearance, adds a layer of depth and interactivity to UI elements. It's a subtle yet powerful way to make your applications feel more dynamic and engaging. In this article, we'll explore how to recreate this stunning effect in your React applications using the liquid-glass-react library. This library p

14 June 2025

10 Best Small Local LLMs to Try Out (< 8GB)

10 Best Small Local LLMs to Try Out (< 8GB)

The world of Large Language Models (LLMs) has exploded, often conjuring images of massive, cloud-bound supercomputers churning out text. But what if you could harness significant AI power right on your personal computer, without constant internet connectivity or hefty cloud subscriptions? The exciting reality is that you can. Thanks to advancements in optimization techniques, a new breed of "small local LLMs" has emerged, delivering remarkable capabilities while fitting comfortably within the me

13 June 2025

React Tutorial: A Beginner's Guide

React Tutorial: A Beginner's Guide

Welcome, aspiring React developer! You've made a fantastic choice. React is a powerful and popular JavaScript library for building user interfaces, and learning it is a surefire way to boost your web development skills. This comprehensive, step-by-step guide will take you from zero to hero, equipping you with the practical knowledge you need to start building your own React applications in 2025. We'll focus on doing, not just reading, so get ready to write some code! 💡Want a great API Testing

13 June 2025

Practice API Design-first in Apidog

Discover an easier way to build and use APIs