shadcn/ui Learning Notes

shadcn/ui Learning Notes

shadcn/ui is a collection of component recipes and a CLI that copies component source code into your project. It is not a traditional component library that you only import from node_modules.

Traditional library
  -> install package
  -> import component
  -> customize through props/theme API

shadcn/ui
  -> run CLI
  -> component source is written into your app
  -> you own and edit the component code

shadcn/ui is popular for modern React and Next.js apps because it combines Tailwind CSS, accessible primitives, local component ownership, and a polished default style.

Read tailwind.md before this note. Read color.md for palette, contrast, dark mode, and theme-token guidance. Read frontend-design.md for design foundations and other UI library choices. Read ui-patterns.md for product screen recipes that use these components.

1. When To Use shadcn/ui

Use shadcn/ui when:

You want good-looking components quickly.
You want component source code in your repository.
You are comfortable with Tailwind CSS.
You want accessible primitives for dialogs, dropdowns, popovers, tabs, and forms.
You are building a SaaS app, dashboard, admin tool, or polished prototype.

Avoid or be careful when:

You want a normal package where updates happen automatically.
You do not want to maintain copied component code.
Your project cannot use Tailwind CSS.
Your team needs a strict enterprise design system such as MUI, Ant Design, or Fluent UI.
You plan to add every component without understanding the generated files.

The biggest mental shift: after the CLI writes a component, it is your component.

2. Setup

For a new or existing React project, initialize shadcn/ui:

# Writes files and changes dependencies
npx shadcn@latest init

Add a component:

# Writes component files and may install dependencies
npx shadcn@latest add button

Add multiple components:

# Writes component files and may install dependencies
npx shadcn@latest add button dialog dropdown-menu form input

Create a new project from a supported template:

# Writes files and installs dependencies
npx shadcn@latest init --template next --name my-app

The current CLI can work with common React frameworks such as Next.js, Vite, React Router, Astro, Laravel, TanStack Start, and related templates. Pick the framework-specific path in the official docs when setting up a real app.

3. What The CLI Creates

Typical files:

components.json
components/ui/button.tsx
components/ui/dialog.tsx
lib/utils.ts
app/globals.css or src/index.css
package.json dependency updates

The exact paths depend on your framework, aliases, and CLI answers.

Typical utility:

// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

cn lets components combine default Tailwind classes, conditional classes, and caller overrides without keeping conflicting utilities.

4. components.json

components.json tells the CLI how your project is structured.

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}

Important fields:

style          Visual style preset. Current docs recommend new-york; old default style is deprecated.
rsc            Whether React Server Component conventions should be used.
tsx            Whether generated files use TypeScript.
tailwind.css   CSS file where theme variables and Tailwind imports live.
tailwind.config For Tailwind v4, leave blank unless your project uses a legacy config.
cssVariables   Recommended for theme tokens.
aliases        Import paths the CLI writes into generated components.

If imports break after adding a component, check that components.json aliases match tsconfig.json or jsconfig.json paths.

5. Component Anatomy

A simple generated component usually combines:

React component
Tailwind class string
cn helper
variants through class-variance-authority when needed
accessible primitive dependency when the pattern is interactive

Button-style shape:

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",
        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",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  },
);

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof buttonVariants>;

export function Button({ className, variant, size, ...props }: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  );
}

Do not memorize this exact code. Learn the pattern:

base classes
variant classes
size classes
default variants
caller className
semantic element

6. Using Components

After adding a component, import it from your local project:

import { Button } from "@/components/ui/button";

export function SaveChanges() {
  return <Button>Save changes</Button>;
}

Dialog shape:

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export function DeleteDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Delete</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Delete project?</DialogTitle>
          <DialogDescription>
            This removes the project for everyone on your team.
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  );
}

Many interactive components use asChild to let the primitive pass behavior to your own element. Make sure the child is a real interactive element when appropriate.

7. Theming

shadcn/ui commonly uses CSS variables for semantic tokens.

@import "tailwindcss";

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --radius: 0.5rem;
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --border: oklch(1 0 0 / 10%);
  --ring: oklch(0.556 0 0);
}

Treat these as product tokens:

background
foreground
primary
secondary
muted
accent
destructive
border
input
ring
radius

Do not hardcode random colors into every component if the design should be themeable. Prefer semantic tokens for shared UI.

8. Tailwind v4 Notes

With Tailwind v4, much configuration moved into CSS. In components.json, tailwind.config can be blank for v4 projects.

{
  "tailwind": {
    "config": "",
    "css": "app/globals.css",
    "cssVariables": true
  }
}

When using Tailwind v3 or an older shadcn setup, you may see tailwind.config.ts, tailwindcss-animate, and older CLI commands in examples. Do not blindly mix v3 and v4 setup snippets.

9. Server and Client Components

In Next.js App Router:

Static visual components can often be Server Components.
Components using state, effects, browser events, or client-only primitives need "use client".
The shadcn CLI may add "use client" where a component needs it.
Do not import server-only code into client components.

Safe pattern:

// app/projects/page.tsx
import { ProjectActions } from "./project-actions";

export default async function ProjectsPage() {
  const projects = await getProjects();

  return (
    <main>
      <ProjectActions />
      {/* render project list */}
    </main>
  );
}
// app/projects/project-actions.tsx
"use client";

import { Button } from "@/components/ui/button";

export function ProjectActions() {
  return <Button onClick={() => console.log("open")}>New project</Button>;
}

10. Forms

shadcn/ui form examples commonly pair React Hook Form and Zod.

# Changes dependencies
npx shadcn@latest add form input button
npm install react-hook-form zod @hookform/resolvers

Example shape:

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

const schema = z.object({
  email: z.string().email(),
});

type FormValues = z.infer<typeof schema>;

export function EmailForm() {
  const form = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: { email: "" },
  });

  function onSubmit(values: FormValues) {
    console.log(values);
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <input {...form.register("email")} />
      {form.formState.errors.email && <p>{form.formState.errors.email.message}</p>}
      <button type="submit">Save</button>
    </form>
  );
}

Client validation is UX. Repeat validation and authorization on the server.

11. Customizing Components

Good customizations:

Rename variants to match product language.
Add sizes that the product actually uses.
Adjust spacing and typography to match the design system.
Wrap low-level components in product-specific components.
Keep semantic HTML and accessibility behavior intact.

Risky customizations:

Removing focus-visible styles.
Replacing buttons with clickable divs.
Deleting labels or descriptions from form fields.
Changing dialog markup without checking focus behavior.
Adding many one-off variants that no screen consistently uses.
Overwriting generated files without reviewing local edits.

Product wrapper example:

import { Badge } from "@/components/ui/badge";

type Status = "paid" | "pending" | "failed";

const statusTone = {
  paid: "bg-green-50 text-green-700 ring-green-600/20",
  pending: "bg-yellow-50 text-yellow-800 ring-yellow-600/20",
  failed: "bg-red-50 text-red-700 ring-red-600/20",
};

export function InvoiceStatus({ status }: { status: Status }) {
  return (
    <Badge className={statusTone[status]} variant="outline">
      {status}
    </Badge>
  );
}

12. Updating Components

Because components are copied into your project, updates are not automatic.

Safe update workflow:

Check git status before adding or updating components.
Read the generated diff.
Keep local product changes unless you intentionally replace them.
Update related dependencies deliberately.
Test keyboard, focus, forms, dialogs, and responsive states.
Commit component updates separately from product feature work when possible.

Useful commands:

# Read-only: inspect local changes first
git status --short

# Writes files and may install dependencies
npx shadcn@latest add button

# Writes files; use carefully if component already exists
npx shadcn@latest add button --overwrite

Use --overwrite only when you are intentionally replacing local component code.

13. Common Components To Learn First

button          actions and links styled as buttons
input           text fields
label           accessible labels
textarea        long-form input
select          option selection
checkbox        binary and multi-select choices
radio-group     exclusive choices
dialog          modal workflows
dropdown-menu   action menus
popover         anchored small panels
tooltip         extra context, not required information
tabs            related views
table           simple table styling
form            form field composition
toast/sonner    transient feedback
sheet/drawer    side panels
command         command palette or searchable menu

Do not add all components at once. Add the component when a screen needs it.

14. Accessibility Checklist

Icon-only buttons have aria-label.
Dialogs have a title and useful description.
Keyboard can open, navigate, submit, cancel, and close workflows.
Focus is visible and restored after closing overlays.
Form errors are associated with fields.
Required fields are clear.
Disabled controls explain why when needed.
Tooltip content is not the only way to understand the UI.
Color is not the only signal for status or error.

Primitive libraries handle many details, but your labels, copy, validation, and product rules still matter.

15. Production Checklist

components.json aliases match tsconfig/jsconfig.
Generated components are committed and reviewed.
Theme tokens match product design.
Dark mode works if enabled.
No component imports server-only modules from a client file.
No secrets are embedded in client-side code.
Forms validate on the server.
Authorization is enforced on the server.
Keyboard and focus behavior are tested for dialogs, menus, popovers, and forms.
Local component changes are documented enough for future updates.

16. Commands To Remember

# Initialize shadcn/ui: writes files and changes dependencies
npx shadcn@latest init

# Add one component: writes files and may install dependencies
npx shadcn@latest add button

# Add several components
npx shadcn@latest add dialog dropdown-menu input form

# Add from a registry URL
npx shadcn@latest add https://ui.shadcn.com/r/styles/new-york/button.json

# Inspect local changes before/after
git status --short
git diff

17. Common Mistakes

Thinking It Updates Like MUI

shadcn/ui copies code into your app. New package versions do not automatically rewrite your local components.

Adding Components Without Reading Them

Read generated code. You are responsible for it after it lands in your repository.

Breaking Accessibility While Styling

Do not remove labels, focus rings, ARIA relationships, keyboard handlers, or primitive wrappers casually.

Overusing Variants

Variants should reflect product decisions. If every screen needs a new variant, the design system is not stable yet.

Mixing Setup Guides

Tailwind v3, Tailwind v4, old shadcn-ui CLI, and current shadcn CLI examples are easy to confuse. Prefer current official docs and match the installed project versions.

Resources