gptdevelopers.io

About gptdevelopers.io/

Table of Contents:

Building GPT Systems & Software / gptdevelopers.io

ShadCN UI: The React Component System That Actually Works/

Michael

Michael

Michael is a software engineer and startup growth expert with 10+ years of software engineering and machine learning experience.

0 Min Read

Twitter LogoLinkedIn LogoFacebook Logo
ShadCN UI: The React Component System That Actually Works
Top No-Code and Low-Code Platforms for Rapid App Development
Top No-Code and Low-Code Platforms for Rapid App Development

Thanks For Commenting On Our Post!

We’re excited to share this comprehensive guide with you. This resource includes best practices, and real-world implementation strategies that we use at slashdev when building apps for clients worldwide.

What’s Inside This Guide:

  • Why ShadCN beats component libraries – Ownership without the bloat
  • Setup in under 2 minutes – From zero to components instantly
  • Top 5 components you’ll use daily – Button, Dialog, Card, Form, Select
  • Real code examples – Copy-paste ready implementations
  • Pro customization tricks – Making it look like your brand, not a template

2. Overview

Component libraries usually force a choice: use their design system and look like everyone else, or fight their constraints for weeks trying to customize everything. ShadCN solves this by not being a library at all.

The difference that matters:

Traditional libraries like Material-UI or Ant Design install as npm packages. You import components, they live in node_modules, and customizing them means overriding styles or ejecting entirely. ShadCN copies component code directly into your project. You own it. The files live in your /components folder where you can modify anything.

Why this approach wins:

You’re not importing <Button> from a package – you’re importing it from your own codebase. Need to change how it works? Edit the file. Want different variants? Add them. No build tool hacks, no CSS specificity wars, no “the library doesn’t support that” limitations.

The technical foundation:

ShadCN components use Radix UI primitives for accessibility and behavior. Radix handles the hard parts – keyboard navigation, ARIA attributes, focus management, screen reader support. Then Tailwind handles the styling with utility classes you already know. You get accessible, responsive components without learning a new API.

What you’re actually installing:

When you run npx shadcn-ui@latest add button, it doesn’t install a package. It copies button.tsx into your project with all the variants pre-configured. That file is yours now. Same with every component – Dialog, Card, Form, Select – they all become source code you control.

The variant system:

Components use class-variance-authority to generate different styles from props. A Button component accepts variant="destructive" or size="lg" and the styling changes automatically. But unlike libraries where variants are fixed, you can add your own. Need a variant="gradient"? Add it to the config. It’s just TypeScript.

Real-world workflow:

You’re building a dashboard. You need a button, a dialog, and a card. Run three commands, get three components. Customize the colors in 30 seconds by editing Tailwind variables. Build your feature without fighting the framework. Ship in hours instead of days.

Composition patterns:

ShadCN components work together naturally. Wrap a Card with a Dialog. Put a Form inside the Card. Add a Select to the Form. Everything composes because they’re just React components with consistent APIs. No “component X doesn’t work inside component Y” gotchas.

When this beats alternatives:

Use ShadCN when you need components fast but want full control. Perfect for startups building MVPs, agencies creating custom dashboards, or developers who value clean code over off-the-shelf solutions. Skip it if you need a complete design system with zero customization – then use Material-UI.

Practical Codes

Code 1: Quick Setup and Essential Components

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
  {
    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",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
        // ADD YOUR CUSTOM VARIANT:
        gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, ...props }, ref) => {
    return (
      <button
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }


// 3. Example: Dashboard with Dialog
// app/dashboard/page.tsx
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function Dashboard() {
  const [open, setOpen] = useState(false)

  return (
    <div className="p-8 space-y-6">
      <h1 className="text-3xl font-bold">Dashboard</h1>
      
      {/* Stats Cards */}
      <div className="grid gap-4 md:grid-cols-3">
        <Card>
          <CardHeader>
            <CardTitle>Total Revenue</CardTitle>
            <CardDescription>+20% from last month</CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-3xl font-bold">$45,231</p>
          </CardContent>
        </Card>
        
        <Card>
          <CardHeader>
            <CardTitle>Active Users</CardTitle>
            <CardDescription>+12% from last month</CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-3xl font-bold">2,431</p>
          </CardContent>
        </Card>
        
        <Card>
          <CardHeader>
            <CardTitle>Conversion Rate</CardTitle>
            <CardDescription>+3% from last month</CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-3xl font-bold">24.5%</p>
          </CardContent>
        </Card>
      </div>

      {/* Action Dialog */}
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogTrigger asChild>
          <Button variant="gradient" size="lg">Create New Project</Button>
        </DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>New Project</DialogTitle>
            <DialogDescription>
              Create a new project to start tracking analytics
            </DialogDescription>
          </DialogHeader>
          <div className="space-y-4 py-4">
            <div className="space-y-2">
              <Label htmlFor="name">Project Name</Label>
              <Input id="name" placeholder="My Awesome Project" />
            </div>
            <div className="space-y-2">
              <Label htmlFor="url">Website URL</Label>
              <Input id="url" placeholder="https://example.com" />
            </div>
          </div>
          <div className="flex justify-end gap-3">
            <Button variant="outline" onClick={() => setOpen(false)}>
              Cancel
            </Button>
            <Button onClick={() => setOpen(false)}>
              Create Project
            </Button>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  )
}

Code 2: Advanced Form with Validation

// app/form-example/page.tsx
"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"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { toast } from "sonner"

// Validation schema
const formSchema = z.object({
  username: z.string().min(3, "Username must be at least 3 characters"),
  email: z.string().email("Invalid email address"),
  role: z.string().min(1, "Please select a role"),
  budget: z.string().min(1, "Budget is required"),
})

export default function FormExample() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      email: "",
      role: "",
      budget: "",
    },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast.success("Form submitted successfully!")
    console.log(values)
  }

  return (
    <div className="flex min-h-screen items-center justify-center p-4">
      <Card className="w-full max-w-md">
        <CardHeader>
          <CardTitle>Create Account</CardTitle>
          <CardDescription>Fill in your details to get started</CardDescription>
        </CardHeader>
        <CardContent>
          <Form {...form}>
            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
              <FormField
                control={form.control}
                name="username"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Username</FormLabel>
                    <FormControl>
                      <Input placeholder="johndoe" {...field} />
                    </FormControl>
                    <FormDescription>Your public display name</FormDescription>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="email"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Email</FormLabel>
                    <FormControl>
                      <Input placeholder="john@example.com" type="email" {...field} />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="role"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Role</FormLabel>
                    <Select onValueChange={field.onChange} defaultValue={field.value}>
                      <FormControl>
                        <SelectTrigger>
                          <SelectValue placeholder="Select your role" />
                        </SelectTrigger>
                      </FormControl>
                      <SelectContent>
                        <SelectItem value="developer">Developer</SelectItem>
                        <SelectItem value="designer">Designer</SelectItem>
                        <SelectItem value="manager">Product Manager</SelectItem>
                        <SelectItem value="founder">Founder</SelectItem>
                      </SelectContent>
                    </Select>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="budget"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Monthly Budget</FormLabel>
                    <Select onValueChange={field.onChange} defaultValue={field.value}>
                      <FormControl>
                        <SelectTrigger>
                          <SelectValue placeholder="Select budget range" />
                        </SelectTrigger>
                      </FormControl>
                      <SelectContent>
                        <SelectItem value="0-1k">$0 - $1,000</SelectItem>
                        <SelectItem value="1k-5k">$1,000 - $5,000</SelectItem>
                        <SelectItem value="5k-10k">$5,000 - $10,000</SelectItem>
                        <SelectItem value="10k+">$10,000+</SelectItem>
                      </SelectContent>
                    </Select>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <Button type="submit" className="w-full" size="lg">
                Create Account
              </Button>
            </form>
          </Form>
        </CardContent>
      </Card>
    </div>
  )
}

4. How to Run the Code

Step 1: Create Next.js project

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

When prompted, select: Yes for ESLint, No for src/ directory, Yes for App Router, No for import alias customization.

Step 2: Initialize ShadCN

npx shadcn-ui@latest init

Choose: New York style, Slate color theme, Yes for CSS variables.

Step 3: Install components

npx shadcn-ui@latest add button card dialog form input label select

Step 4: Install additional dependencies

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

Step 5: Add toast provider

Edit app/layout.tsx and add Sonner:

import { Toaster } from "sonner"

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  )
}

Step 6: Create dashboard page

Create app/dashboard/page.tsx and paste Code 1’s dashboard example.

Step 7: Create form page

Create app/form-example/page.tsx and paste Code 2.

Step 8: Run development server

npm run dev

Step 9: View your pages

  • Dashboard: http://localhost:3000/dashboard
  • Form: http://localhost:3000/form-example

Step 10: Customize your first component

Open components/ui/button.tsx and add a new variant:

variants: {
  variant: {
    // existing variants...
    success: "bg-green-500 text-white hover:bg-green-600",
  }
}

Use it: <Button variant="success">Success Button</Button>


5. Key Concepts

ShadCN isn’t a library you install – it’s a pattern you adopt. Components live in your codebase, not in node_modules, which means you control everything.

Why this matters: Traditional libraries force you to work around their decisions. ShadCN gives you the starting point and gets out of your way. The Button component is 40 lines of TypeScript you can read and modify in 2 minutes.

The variant system scales: Adding variant="gradient" took one line of code. Try doing that with Material-UI without fighting their theme system for an hour. Class-variance-authority makes variants feel like props, but the implementation is just conditional CSS classes.

Composition beats configuration: Instead of a massive <Table> component with 50 props, ShadCN gives you small pieces that combine naturally. This is how React was meant to be used.

About slashdev.io

At slashdev.io, we’re a global software engineering company specializing in building production web and mobile applications. We combine cutting-edge LLM technologies (Claude Code, Gemini, Grok, ChatGPT) with traditional tech stacks like ReactJS, Laravel, iOS, and Flutter to deliver exceptional results.

What sets us apart:

  • Expert developers at $50/hour
  • AI-powered development workflows for enhanced productivity
  • Full-service engineering support, not just code
  • Experience building real production applications at scale

Whether you’re building your next app or need expert developers to join your team, we provide ongoing developer relationships that go beyond one-time assessments.

Need Development Support?

Building something ambitious? We’d love to help. Our team specializes in turning ideas into production-ready applications using the latest AI-powered development techniques combined with solid engineering fundamentals.