Shadcn UI Tutorial: Build Modern React UIs with Complete Control

Discover how Shadcn UI empowers React developers to build fully customizable, production-grade interfaces with complete control. Learn setup, theming, advanced forms, and best practices—all without sacrificing speed or flexibility.

Audrey Lopez

Audrey Lopez

16 January 2026

Shadcn UI Tutorial: Build Modern React UIs with Complete Control

Modern web development demands both speed and flexibility when building polished user interfaces. Traditional React UI libraries like Material-UI, Ant Design, and Chakra UI offer convenience but often force developers to compromise on control, customizability, and bundle size.

Shadcn UI introduces a new approach: instead of installing a bulky dependency, you copy well-crafted, accessible components directly into your project. This empowers your team to tailor every detail while benefiting from a robust foundation.

In this comprehensive guide, you'll learn how to:

💡 Looking for an API platform that helps you deliver as seamlessly as Shadcn UI? Generate beautiful API documentation and boost your team's productivity with integrated collaboration tools. Apidog is your all-in-one solution—replace Postman for a better price!

button

Why Shadcn UI? Solving Real Developer Pain Points

Before diving into code, it's vital to grasp why Shadcn UI exists:

Shadcn UI Is Not:

What Makes Shadcn UI Unique

This model is ideal for API-driven teams who value code ownership, custom branding, and accessibility—while maintaining rapid development velocity.


Getting Started: Project Setup with Shadcn UI

Let's set up a new project using Next.js (recommended for API-focused apps). We'll also briefly cover Vite for those using React without Next.js.

Prerequisites

1. Create Your Next.js Project

In your terminal, run:

npx create-next-app@latest my-shadcn-app --typescript --tailwind --eslint

Recommended setup choices:

Navigate to your app:

cd my-shadcn-app

2. Initialize Shadcn UI

From your project root, run:

npx shadcn-ui@latest init

You'll answer a series of prompts:

The CLI will:

Your project is now Shadcn-ready!

Vite Setup?

Follow a similar process—after scaffolding your Vite + React + TS app, install Tailwind, then run npx shadcn-ui@latest init. The CLI will guide you based on your file structure.


Building with Shadcn UI: From Simple to Advanced

1. Add Your First Components

Clean up the Next.js boilerplate. Start with a Button:

npx shadcn-ui@latest add button

This creates src/components/ui/button.tsx.

Add a Card:

npx shadcn-ui@latest add card

Now, in src/app/page.tsx, use these components:

import { Button } from "@/components/ui/button";
import {
  Card, CardHeader, CardTitle, CardDescription,
  CardContent, CardFooter
} from "@/components/ui/card";
import { Input } from "@/components/ui/input"; // Add next
import { Label } from "@/components/ui/label"; // Add next

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>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>
            <Input id="framework" placeholder="e.g. Next.js" />
          </div>
        </CardContent>
        <CardFooter>
          <Button className="w-full">Deploy</Button>
        </CardFooter>
      </Card>
    </main>
  );
}

If you haven't yet:

npx shadcn-ui@latest add input
npx shadcn-ui@latest add label
npm run dev

Check your app at http://localhost:3000.

2. Add Interactive Components

Replace the static "Framework" input with a Select dropdown:

npx shadcn-ui@latest add select

The CLI ensures dependencies like Popover are present.

Update your form:

import {
  Select, SelectTrigger, SelectValue, SelectContent, SelectItem
} from "@/components/ui/select";

// ...within 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>

Add Toast Notifications

Want feedback when "Deploy" is clicked?

npx shadcn-ui@latest add toast

In src/app/layout.tsx:

import { Toaster } from "@/components/ui/toaster";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  );
}

In your page, enable client-side code and add a toast on button click:

'use client';
import { useToast } from "@/components/ui/use-toast";

export default function Home() {
  const { toast } = useToast();

  function handleDeploy() {
    toast({
      title: "Deployment Scheduled!",
      description: "Your project is being deployed.",
      duration: 5000,
    });
  }

  // ... return JSX ...
  <Button className="w-full" onClick={handleDeploy}>Deploy</Button>
}

Building Forms with Validation (react-hook-form + zod)

Complex apps need robust, type-safe forms. Shadcn UI pairs seamlessly with react-hook-form and zod.

1. Install Form Dependencies

npm install react-hook-form zod @hookform/resolvers

2. Add Shadcn Form Component

npx shadcn-ui@latest add form

3. Create a Validation Schema

In your page:

import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
  Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage
} from "@/components/ui/form";

const formSchema = z.object({
  projectName: z.string().min(2, "Project name must be at least 2 characters.").max(50),
  framework: z.string({ required_error: "Please select a framework." }),
});

4. Wire Up the Form

export default function Home() {
  const { toast } = useToast();
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { projectName: "" },
  });

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast({
      title: "Submitted 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>
      ),
    });
  }

  return (
    <main className="flex min-h-screen items-center justify-center bg-background p-8">
      <Card className="w-full max-w-md">
        <CardHeader>
          <CardTitle>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 />
                  </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>
  );
}

Try submitting with empty fields to see validation in action!


Advanced Customization: Theming and Component Editing

1. Global Theming with CSS Variables

Open src/app/globals.css and locate the :root and .dark blocks. These HSL variables control your entire color palette and border radii.

:root {
  --background: 0 0% 100%;
  --primary: 222.2 47.4% 11.2%;
  --radius: 0.5rem;
  /* ... */
}
.dark {
  --background: 222.2 84% 4.9%;
  --primary: 210 40% 98%;
  /* ... */
}

2. Enable Dark Mode (Next.js Example)

Install a theme switcher:

npm install next-themes

Create src/components/theme-provider.tsx:

"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes";

export function ThemeProvider({ children, ...props }) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

Wrap your root layout:

import { ThemeProvider } from "@/components/theme-provider";

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
          {children}
          <Toaster />
        </ThemeProvider>
      </body>
    </html>
  );
}

Add a toggle button (src/components/mode-toggle.tsx):

"use client"
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>
  );
}

Place <ModeToggle /> anywhere in your app for instant dark mode.

3. Editing Component Source: Example (Button Variant)

Need a custom button variant? Edit src/components/ui/button.tsx:

const buttonVariants = cva(
  // ...base styles
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        success: "bg-green-600 text-white hover:bg-green-600/90", // New!
        // ... other variants
      },
      // ...size variants
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

Now use: <Button variant="success">Success</Button>


Best Practices for Large-Scale Apps


Conclusion: Become the Author of Your UI Library

Shadcn UI gives you total control—every component is yours to adapt, extend, and perfect. By leveraging the strengths of Tailwind CSS and Radix UI, it empowers API-focused teams to build scalable, accessible, and uniquely branded UIs at top speed.

For developer teams who value ownership and productivity, this approach is transformative. And when your backend and API tooling needs to keep pace, consider how Apidog supports collaboration and seamless API documentation so your entire stack stays developer-first and efficient. Boost your team's productivity and upgrade your API workflow at a better price.

button

Explore more

Top 5 Voice Clone APIs In 2026

Top 5 Voice Clone APIs In 2026

Explore the top 5 voice clone APIs transforming speech synthesis. Compare them with their features, and pricing. Build voice-powered applications with confidence.

27 January 2026

Top 5 Text-to-Speech and Speech-to-Text APIs You Should Use Right Now

Top 5 Text-to-Speech and Speech-to-Text APIs You Should Use Right Now

Discover the 5 best TTS APIs and STT APIs for your projects. Compare features, pricing, and performance of leading speech technology platforms. Find the perfect voice API solution for your application today.

26 January 2026

How to Use Claude Code for CI/CD Workflows

How to Use Claude Code for CI/CD Workflows

Technical guide to integrating Claude Code into CI/CD pipelines. Covers container setup, GitHub Actions/GitLab CI integration, skill development, and practical workflows for DevOps automation.

21 January 2026

Practice API Design-first in Apidog

Discover an easier way to build and use APIs