// As a front-end dev, I’ve explored Shadcn UI—a minimalist, powerful toolkit for control and efficiency.
As a front-end dev, I’ve been diving into Shadcn UI—a minimalist toolkit that blends power and control. It’s a fresh take on UI libraries, perfect for modern workflows.
Shadcn UI isn’t here to hand you a bloated, opinionated design system. Instead, it’s a "bring-your-own-components" toolkit built on Tailwind CSS and Radix UI primitives. You get pre-styled, accessible components as a starting point—not a finish line. The source code gets copied into your project, meaning you own it entirely. No black-box dependencies, no runtime CSS-in-JS overhead—just lean, maintainable code.
Let’s set it up in a Next.js project and push it further than a simple button. I’ll assume you’ve got a basic Next.js app with Tailwind already configured (if not, check the Tailwind docs).
Initialize Shadcn:
npx shadcn-ui@latest init
This scaffolds a components/ui directory and tweaks your tailwind.config.js to include CSS variables (optional but recommended). Add some components:
npx shadcn-ui@latest add button dropdown-menu dialog
Here’s what the generated button.tsx looks like (simplified):
import { cva, type VariantProps } from "class-variance-authority";
import { forwardRef } from "react";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-input hover:bg-accent",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={buttonVariants({ variant, size, className })}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
What’s happening here? CVA (Class Variance Authority): A utility for defining variant-based styles in a type-safe way. ForwardRef: Ensures the component can handle refs, which is clutch for focus management or animations. Tailwind-Driven: The base styles and variants use Tailwind classes, keeping things predictable.
Let’s create a dropdown-triggered dialog—a common UI pattern that shows off Shadcn’s composability.
// app/components/Header.tsx
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export default function Header() {
return (
<header className="p-4 border-b">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Menu</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
Open Settings
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>
Customize your app preferences here.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<label className="block">
Theme
<select className="mt-1 block w-full rounded-md border p-2">
<option>Light</option>
<option>Dark</option>
</select>
</label>
<Button>Save</Button>
</div>
</DialogContent>
</Dialog>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
);
}
DropdownMenu: A headless Radix primitive styled with Tailwind. The asChild prop lets you wrap it around a Button for triggering. Dialog: Another Radix-powered component. Nesting it inside the dropdown shows how Shadcn components play nicely together. Custom Styling: I’ve kept it minimal, but you could add className="bg-[hsl(var(--background))]" to the dialog for theme consistency. Run this in your app (app/page.tsx):
import Header from "@/components/Header";
export default function Home() {
return (
<div>
<Header />
<main className="p-4">Welcome to my app!</main>
</div>
);
}
Want a custom button variant? Add it to buttonVariants:
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-input hover:bg-accent",
gradient: "bg-gradient-to-r from-blue-500 to-purple-500 text-white",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
Then use it:
<Button variant="gradient">Gradient Button</Button>
Need a dark mode? Update your globals.css:
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3%;
}
@media (prefers-color-scheme: dark) {
:root {
--background: 0 0% 10%;
--foreground: 0 0% 98%;
}
}
Shadcn’s CSS variables make this seamless.
Shadcn UI feels like a toolkit built by front-end devs for front-end devs. It respects your time and expertise, handing you the keys to a lightweight, extensible system. Is it perfect? No—nothing is. But for projects where I want to ship fast, maintain control, and keep performance tight, it’s become my go-to. If you’re a dev who loves digging into the guts of your tools, give it a spin.
Resources