ReactUIRadixTailwind

shadcn/ui: The New Standard 🧱

s
shadcn
Featured Guide 30 min read

"I used to wait 3 months for Material UI to fix a bug. Now I fix it in 3 minutes."

shadcn/ui is not a library you install in package.json. It is a CLI that purely copies code into your project. You own the component. You own the styles. You own the bugs. This is the ultimate freedom for a frontend engineer.

We call it "The Copy/Paste Revolution". Instead of fighting with a black-box NPM package to change a border-radius, you just open the file and change the class. It builds upon Radix UI (Headless) and Tailwind CSS (Styling) to give you the best of both worlds: Accessibility and Customizability.

02. Headless Architecture

Separation of Concerns re-imagined. We separate the Behavior from the Style.

🧠 Radix UI (The Brain)

"I handle the open/close state, the collision detection, the focus trap, and the ARIA attributes. I am invisible."

🎨 Tailwind CSS (The Skin)

"I handle how it looks. I don't care if it's open or closed, I just style the `data-state` attribute."

03. Class Variance Authority (CVA)

How do we handle variants like `primary`, `secondary`, `outline`, or sizes like `sm`, `lg` without messy template literals? Enter cva. It's a schema for your CSS classes.

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors", 
  {
    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",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

04. CSS Variables: The Secret Sauce

Notice in the CVA example above, we use `bg-primary`, not `bg-blue-500`. This is because shadcn uses Semantic Tokens mapped to CSS Variables using HSL values.

// globals.css
:root {
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
}

.dark {
  --primary: 210 40% 98%;
  --primary-foreground: 222.2 47.4% 11.2%;
}

Pro Tip: This allows Dark Mode to work automatically without adding `dark:` modifiers everywhere. We just swap the values of the variables.

05. How the CLI Works

When you run `npx shadcn-ui@latest add button`, it does three simple things:

1

Fetch

Fetches the component code from the registry (GitHub raw files).

2

Check

Checks your `components.json` config to see where to put it.

3

Install

Installs any necessary dependencies (like `@radix-ui/react-slot`).

06. Share the Freedom

💡

Did You Know?

Shadcn isn't paid work. It's an open source project that changed the entire React ecosystem. You can build your own registry too!

Twitter / X Caption

"Stop npm installing heavy UI libraries. Copy/Paste the code and own your UI. Shadcn/ui changed how I build apps. #React #WebDev #Tailwind"

LinkedIn Post

"I deleted 400MB of node_modules by switching to Shadcn/ui. Headless accessibility + Tailwind customization is the holy grail of UI development. #Frontend #Architecture"

The Takeaway

Ownership > Abstraction. Your design system should live in your codebase, not in `node_modules`.

#DesignSystem #UI #Radix

07. Theme Playground

Shadcn uses CSS variables for theming. Change the "Primary" color variable and watch the UI update globally.

System Radius & Color

This entire playground is driven by 2 React state variables that inject inline styles to mimic CSS variables.

Interactive Playground

import React, { useState } from 'react';

// ----------------------------------------------------
// 🧱 SHADCN THEME BUILDER
// ----------------------------------------------------

export default function ShadcnThemer() {
  const [radius, setRadius] = useState('0.5rem');
  const [color, setColor] = useState('zinc'); // orange, blue, green, zinc

  // Map simulated CSS variables
  const getColorClass = () => {
      if(color === 'orange') return 'bg-orange-500 text-white shadow-orange-500/20 hover:bg-orange-600';
      if(color === 'blue') return 'bg-blue-600 text-white shadow-blue-600/20 hover:bg-blue-700';
      if(color === 'green') return 'bg-emerald-600 text-white shadow-emerald-600/20 hover:bg-emerald-700';
      return 'bg-zinc-900 dark:bg-zinc-50 text-white dark:text-zinc-900 shadow-zinc-900/10 hover:bg-zinc-800 dark:hover:bg-zinc-200';
  }

  const getBorderClass = () => {
      // Represents 'ring'
      if(color === 'orange') return 'focus:ring-orange-500';
      if(color === 'blue') return 'focus:ring-blue-600';
      if(color === 'green') return 'focus:ring-emerald-600';
      return 'focus:ring-zinc-900 dark:focus:ring-zinc-100';
  }

  return (
    <div className="bg-white dark:bg-[#111] text-gray-900 dark:text-gray-200 border border-gray-200 dark:border-gray-800 rounded-2xl overflow-hidden shadow-2xl p-8 flex flex-col md:flex-row gap-8 h-[500px]">
      
      {/* Controls */}
      <div className="w-full md:w-1/3 space-y-6">
          <div className="space-y-4">
               <h3 className="text-xl font-bold">1. Theme Variables</h3>
               
               <div className="space-y-2">
                   <label className="text-xs font-bold uppercase text-gray-500">Primary Color</label>
                   <div className="flex gap-2">
                       {['zinc', 'orange', 'blue', 'green'].map(c => (
                           <button key={c} onClick={() => setColor(c)} className={`w-8 h-8 rounded-full ${c==='zinc' ? 'bg-zinc-900' : `bg-${c}-500`} ring-offset-2 ${color === c ? 'ring-2 ring-gray-400' : ''}`}></button>
                       ))}
                   </div>
               </div>

                <div className="space-y-2">
                   <label className="text-xs font-bold uppercase text-gray-500">Radius</label>
                   <div className="flex items-center gap-4">
                       <input type="range" min="0" max="1.5" step="0.25" onChange={(e) => setRadius(`${e.target.value}rem`)} className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-black dark:accent-white"/>
                       <span className="font-mono text-xs w-12 text-right">{radius}</span>
                   </div>
               </div>
          </div>

          <div className="p-4 bg-gray-50 dark:bg-zinc-900 rounded-xl font-mono text-xs text-gray-500 space-y-1">
              <div>--radius: <span className="text-black dark:text-white">{radius}</span>;</div>
              <div>--primary: <span className="text-black dark:text-white">{color}</span>;</div>
          </div>
      </div>

      {/* Visualization */}
      <div className="flex-1 bg-gray-50 dark:bg-black/50 border border-gray-200 dark:border-gray-800 rounded-2xl flex flex-col items-center justify-center relative p-12 gap-6">
           
           {/* Card Component */}
           <div 
             className="bg-white dark:bg-black border border-gray-200 dark:border-gray-800 p-8 w-full max-w-sm shadow-xl"
             style={{ borderRadius: radius }}
           >
               <h3 className="font-bold text-lg mb-2">Payment Details</h3>
               <p className="text-sm text-gray-500 mb-6">Enter your credentials to continue.</p>
               
               <div className="space-y-4 mb-6">
                   <input 
                     placeholder="Card Number" 
                     className={`w-full px-3 py-2 border border-gray-200 dark:border-gray-800 outline-none focus:ring-2 ${getBorderClass()} transition-all`}
                     style={{ borderRadius: `calc(${radius} - 2px)` }}
                   />
                   <div className="flex gap-4">
                        <input 
                            placeholder="MM/YY" 
                            className={`w-1/2 px-3 py-2 border border-gray-200 dark:border-gray-800 outline-none focus:ring-2 ${getBorderClass()} transition-all`}
                            style={{ borderRadius: `calc(${radius} - 2px)` }}
                        />
                         <input 
                            placeholder="CVC" 
                            className={`w-1/2 px-3 py-2 border border-gray-200 dark:border-gray-800 outline-none focus:ring-2 ${getBorderClass()} transition-all`}
                            style={{ borderRadius: `calc(${radius} - 2px)` }}
                        />
                   </div>
               </div>

               <button 
                className={`w-full py-2.5 font-bold text-sm transition-all shadow-lg active:scale-95 ${getColorClass()}`}
                style={{ borderRadius: `calc(${radius} - 2px)` }}
               >
                   Confirm Payment
               </button>
           </div>
      </div>

    </div>
  );
}