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 line | Restyle from the outside only |
| No runtime dep, no black box | One import, less code in repo |
| You apply updates manually | Updates via npm update |
| Built on Tailwind + Radix you already know | Its 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/... và @/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 {ghicomponents.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.tswith thecn()helper (exactly the one from Part 5) {tạolib/utils.tsvới helpercn()(đú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}:
- copies each component’s source into
components/ui/{copy mã nguồn mỗi component vàocomponents/ui/} - installs the Radix packages it needs automatically (e.g.
@radix-ui/react-avatar) {tự cài các package Radix nó cần} - adds
cva,clsx,tailwind-mergeif missing {thêmcva,clsx,tailwind-mergenế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}.
initwritescomponents.json, theme tokens, andcn();addcopies components and auto-installs their Radix deps {initghicomponents.json, token theme, vàcn();addcopy component và tự cài dep Radix}.- The generated components are exactly the Tailwind + Radix +
cva/cnpatterns from this series {Component sinh ra đúng là các mẫu Tailwind + Radix +cva/cntrong 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}.