jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Tailwind, Radix & shadcn/ui · Part 9 — shadcn/ui: Philosophy & Setup

Why shadcn is "not a component library", how the CLI and components.json work, and adding your first components — the moment Tailwind, Radix and cva/cn click into one workflow. With a live component gallery.

Everything so far has been building to this {Mọi thứ tới giờ đều dẫn tới đây}. You know Tailwind, you know Radix, you know cva + cn {Bạn biết Tailwind, biết Radix, biết cva + cn}. shadcn/ui is what happens when someone assembles those three, beautifully, for ~50 common components — and then gives you the source code instead of a package {shadcn/ui là kết quả khi ai đó ráp ba thứ đó, đẹp đẽ, cho ~50 component phổ biến — rồi đưa bạn mã nguồn thay vì một package}.


1. “Not a component library” — the key idea {“Không phải thư viện component” — ý tưởng cốt lõi}

Traditional libraries (MUI, Ant, Chakra) are npm packages {Thư viện truyền thống (MUI, Ant, Chakra) là package npm}. You npm install, import <Button>, and the component lives in node_modules — a black box you configure from the outside and fight to restyle {Bạn npm install, import <Button>, và component nằm trong node_modules — một hộp đen bạn cấu hình từ ngoài và vật lộn để restyle}.

shadcn/ui inverts this {shadcn/ui đảo ngược điều này}:

You don’t install components — you copy their source code into your project. {Bạn không cài component — bạn copy mã nguồn của chúng vào project.}

A CLI drops the actual .tsx file into components/ui/ {Một CLI thả file .tsx thật vào components/ui/}. It’s your file now {Giờ nó là file của bạn}. Edit it, delete a variant, change the markup, add a prop — no overrides, no !important, no version-lock fights {Sửa nó, xóa một variant, đổi markup, thêm prop — không override, không !important, không kẹt version}.

The trade-offs, honestly {Đánh đổi, nói thật}:

Copy-in (shadcn)Package (MUI/Ant)
You own & can edit every lineRestyle from the outside only
No runtime dep, no black boxOne import, less code in repo
You apply updates manuallyUpdates via npm update
Built on Tailwind + Radix you already knowIts own styling system to learn

For teams that already use Tailwind, the ownership model is a huge win {Với team đã dùng Tailwind, mô hình sở hữu là lợi thế lớn}. See the out-of-the-box look you get {Xem vẻ ngoài bạn có ngay}:


2. Prerequisites {Điều kiện cần}

shadcn assumes the stack we built across this series {shadcn giả định stack ta đã dựng xuyên series}: a React project (Vite, Next.js, etc.), Tailwind configured, TypeScript, and a path alias {một project React, Tailwind đã cấu hình, TypeScript, và một path alias}. The alias matters — components import each other via @/components/... and @/lib/utils {Alias quan trọng — các component import nhau qua @/components/...@/lib/utils}:

// tsconfig.json
{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }

3. init — one-time setup {init — cài một lần}

npx shadcn@latest init

The CLI asks a few questions (base color, CSS file location, alias) and then {CLI hỏi vài câu (màu nền, vị trí file CSS, alias) rồi}:

  • writes components.json — the project manifest {ghi components.json — manifest của project}
  • adds CSS variables for the theme into your stylesheet (the semantic tokens from Part 3) {thêm biến CSS cho theme vào stylesheet (token ngữ nghĩa từ Phần 3)}
  • creates lib/utils.ts with the cn() helper (exactly the one from Part 5) {tạo lib/utils.ts với helper cn() (đúng cái ở Phần 5)}

components.json records your choices so future add commands know where to put files and which conventions to follow {components.json ghi lại lựa chọn để các lệnh add sau biết đặt file ở đâu và theo quy ước nào}:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tailwind": {
    "css": "src/index.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "ui": "@/components/ui",
    "utils": "@/lib/utils"
  },
  "iconLibrary": "lucide"
}

4. add — pull in components {add — kéo component vào}

npx shadcn@latest add button card input badge avatar alert

This {Lệnh này}:

  1. copies each component’s source into components/ui/ {copy mã nguồn mỗi component vào components/ui/}
  2. installs the Radix packages it needs automatically (e.g. @radix-ui/react-avatar) {tự cài các package Radix nó cần}
  3. adds cva, clsx, tailwind-merge if missing {thêm cva, clsx, tailwind-merge nếu thiếu}

Now you just import and use them {Giờ chỉ việc import và dùng}:

import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';

export function CreateProject() {
  return (
    <Card className="max-w-sm">
      <CardHeader><CardTitle>Create project</CardTitle></CardHeader>
      <CardContent className="flex justify-end gap-2">
        <Button variant="ghost">Cancel</Button>
        <Button>Deploy</Button>
      </CardContent>
    </Card>
  );
}

5. Read the generated button.tsx — you already know it {Đọc button.tsx sinh ra — bạn đã biết rồi}

This is the payoff of the whole series {Đây là phần thưởng của cả series}. Open the file the CLI created and you’ll recognize every line {Mở file CLI tạo và bạn nhận ra từng dòng}:

import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';          // Part 8 — asChild
import { cva, type VariantProps } from 'class-variance-authority'; // Part 5
import { cn } from '@/lib/utils';                       // Part 5

const buttonVariants = cva(
  'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium ' +
  'transition-colors focus-visible:outline-none focus-visible:ring-2 ' +
  'disabled:pointer-events-none disabled:opacity-50',   // Parts 2 & 4
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90', // Part 3 tokens
        destructive: 'bg-destructive text-white hover:bg-destructive/90',
        outline: 'border border-input bg-background hover:bg-accent',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: { default: 'h-9 px-4 py-2', sm: 'h-8 px-3', lg: 'h-10 px-6', icon: 'size-9' },
    },
    defaultVariants: { variant: 'default', size: 'default' },
  }
);

function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
  const Comp = asChild ? Slot : 'button';                 // Part 8
  return <Comp className={cn(buttonVariants({ variant, size }), className)} {...props} />; // Part 5
}

There is no magic here {Không có phép màu nào ở đây}. cva for variants, cn for merging, Slot for asChild, Tailwind utilities for looks, semantic tokens (bg-primary) for theming {cva cho variant, cn cho merge, Slot cho asChild, utility Tailwind cho vẻ ngoài, token ngữ nghĩa (bg-primary) cho theming}. You learned every piece — shadcn just assembled them tastefully {Bạn đã học từng mảnh — shadcn chỉ ráp lại có gu}.


6. Exercises {Bài tập}

1. In one sentence, how is shadcn fundamentally different from MUI? {Một câu, shadcn khác MUI căn bản ở chỗ nào?}

Solution {Lời giải}

shadcn copies editable component source into your repo; MUI ships components as an npm package you import and configure from outside {shadcn copy mã nguồn component sửa được vào repo; MUI ship component dưới dạng package npm bạn import và cấu hình từ ngoài}.

2. What three things does npx shadcn init create/modify? {npx shadcn init tạo/sửa ba thứ gì?}

Solution {Lời giải}

components.json, theme CSS variables in your stylesheet, and lib/utils.ts (the cn helper) {components.json, biến CSS theme trong stylesheet, và lib/utils.ts (helper cn)}.

3. After add dialog, which dependency gets installed automatically and why? {Sau add dialog, dependency nào được cài tự động và vì sao?}

Solution {Lời giải}

@radix-ui/react-dialog — the component is a styled wrapper around that Radix primitive {@radix-ui/react-dialog — component là wrapper đã style quanh primitive Radix đó}.

Stretch {Nâng cao}: map each line of the generated button.tsx above to the series part that taught it {ánh xạ từng dòng của button.tsx ở trên với phần series đã dạy nó}.


Key takeaways {Điểm chính}

  • shadcn is a distribution of source code, not an installed library — you own and edit every component {shadcn là bản phân phối mã nguồn, không phải thư viện cài đặt — bạn sở hữu và sửa mọi component}.
  • init writes components.json, theme tokens, and cn(); add copies components and auto-installs their Radix deps {init ghi components.json, token theme, và cn(); add copy component và tự cài dep Radix}.
  • The generated components are exactly the Tailwind + Radix + cva/cn patterns from this series {Component sinh ra đúng là các mẫu Tailwind + Radix + cva/cn trong series này}.

Next up {Tiếp theo}

Part 10 — Theming & customizing shadcn: how the CSS-variable theme works, building light/dark, generating a brand theme, customizing a component, and the registry system for sharing your own components {Phần 10 — Theme & tùy biến shadcn: cách theme biến-CSS hoạt động, dựng light/dark, tạo theme brand, tùy biến component, và hệ registry để chia sẻ component của bạn}.