Shadcn UI là gì? Hướng dẫn cài đặt và làm chủ Shadcn UI trong Next.js App Router
Shadcn UI là bộ mã nguồn component viết sẵn bằng Radix UI Primitives và Tailwind CSS, được phân phối qua công cụ dòng lệnh chép thẳng file vào dự án của bạn, thay vì cài đặt như một gói phụ thuộc từ npm. Nói ngắn gọn: bạn không "dùng" Shadcn UI, bạn sở hữu mã nguồn của nó.
Sự khác biệt nghe có vẻ nhỏ nhưng thay đổi toàn bộ cách một dự án Next.js được xây và bảo trì. Bài viết này đi từ bản chất kiến trúc, cách cài đặt vào App Router, tới những cạm bẫy mà tài liệu chính thức không nói rõ. Nếu bạn đang phân vân chọn giữa hai trường phái, hãy đọc thêm bài so sánh Shadcn UI với Ant Design v5.
1. Shadcn UI là gì và không phải là gì
Điểm gây nhầm lẫn lớn nhất với người mới: gõ npm install shadcn-ui để cài các component là sai ngay từ đầu. Không có gói component nào nằm trên npm để import. Thứ nằm trên npm chỉ là công cụ dòng lệnh làm nhiệm vụ tải file về máy bạn.
| Người ta thường nghĩ | Thực tế |
|---|---|
| Là thư viện UI như MUI, Antd, Chakra | Là một bộ sưu tập mã nguồn (registry) kèm công cụ chép file |
| Import component từ node_modules | Import từ chính thư mục dự án, ví dụ components/ui/button |
| Nâng cấp bằng npm update | Nâng cấp thủ công từng component, có kiểm soát |
| Bị khóa vào một hệ thiết kế cố định | Sửa trực tiếp file, không cần override CSS hay ghi đè token |
| Xóa đi thì gãy toàn dự án | Xóa công cụ CLI đi thì mã nguồn vẫn chạy bình thường |
Hệ quả rất thực tế: dự án của bạn không bao giờ gặp cảnh tác giả thư viện bỏ ngang bảo trì, hoặc bản nâng cấp lớn đổi API khiến 40 màn hình vỡ giao diện cùng lúc. Cái giá phải trả nằm ở mục 9 phía dưới.
2. Bốn lớp công nghệ nằm dưới mỗi component
Mỗi file component chép về là sự lắp ghép của bốn thư viện độc lập. Hiểu vai trò từng lớp là điều kiện bắt buộc để sửa được component khi cần.
| Lớp | Thư viện | Chịu trách nhiệm |
|---|---|---|
| Hành vi và trợ năng | Radix UI Primitives | Bẫy tiêu điểm bàn phím, thuộc tính ARIA, điều hướng mũi tên, khóa cuộn nền, quản lý lớp phủ. Đây là phần khó nhất và cũng là phần bạn không nên tự viết lại |
| Giao diện | Tailwind CSS | Toàn bộ màu, khoảng cách, bo góc, đổ bóng nằm trong thuộc tính className |
| Biến thể | class-variance-authority (CVA) | Khai báo variant và size dưới dạng dữ liệu, sinh ra kiểu chữ TypeScript cho props tương ứng |
| Gộp lớp | clsx và tailwind-merge (hàm cn) | Gộp className truyền từ ngoài vào và tự loại bỏ lớp Tailwind xung đột, ví dụ px-4 gặp px-8 thì giữ px-8 |
Radix Primitives là component không có giao diện: nó chỉ cung cấp hành vi và cấu trúc DOM đúng chuẩn WAI-ARIA. Đó là lý do một Dialog chép từ Shadcn UI về đã xử lý sẵn phím Esc, bẫy tiêu điểm và thuộc tính aria-modal mà bạn không phải viết dòng nào. Xem thêm bài tiêu chuẩn trợ năng WCAG 2.1 để hiểu vì sao phần này đắt giá.
3. Hàm cn: chi tiết nhỏ quyết định khả năng tùy biến
Đây là toàn bộ nội dung file lib/utils, nơi mọi component đều import vào:
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Không có hàm này, khi bạn viết <Button className="bg-red-500"> thì lớp nền đỏ và lớp nền mặc định của Button sẽ cùng tồn tại trong thuộc tính class, và trình duyệt chọn cái nào là do thứ tự khai báo trong file CSS đã biên dịch quyết định, không phải do bạn quyết định. tailwind-merge giải bài toán đó bằng cách hiểu ngữ nghĩa từng nhóm lớp Tailwind và giữ lại lớp đứng sau.
4. Cài đặt vào dự án Next.js App Router
Quy trình chuẩn gồm ba lệnh. Lưu ý tên gói CLI đã đổi từ shadcn-ui thành shadcn, tài liệu cũ trên mạng vẫn còn ghi tên cũ:
# 1. Tạo dự án Next.js (bỏ qua nếu đã có sẵn)
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
# 2. Khởi tạo cấu hình, lệnh này sinh ra components.json và lib/utils.ts
npx shadcn@latest init
# 3. Chép từng component về dự án
npx shadcn@latest add button dialog input form table
Lệnh init sẽ hỏi vài câu về style, màu nền và vị trí file CSS toàn cục. Sau khi chạy xong, hãy mở thư mục components/ui để thấy các file vừa được chép về. Đó là mã nguồn của bạn, được phép sửa thoải mái.
Một mẹo ít người biết: CLI nhận cả đường dẫn URL, nên bạn có thể cài component từ registry của bên thứ ba hoặc registry nội bộ của công ty bằng cách truyền thẳng địa chỉ thay cho tên component.
5. Giải phẫu file components.json
File này là bản đồ để CLI biết chép file vào đâu và biên dịch theo quy ước nào. Sửa sai một trường là lần chạy lệnh add tiếp theo sẽ rải file lung tung.
| Trường | Ý nghĩa | Lưu ý khi sửa |
|---|---|---|
| style | Bộ style dùng chung cho mọi component | Chỉ có tác dụng lúc chép file mới, đổi về sau không hồi tố các file đã có |
| rsc | Có thêm dòng use client vào đầu component cần tương tác hay không | Đặt true với App Router, đặt false với Pages Router |
| tsx | Sinh file TypeScript hay JavaScript | Dự án JavaScript thuần phải đặt false, nếu không sẽ chép về file .tsx không biên dịch được |
| tailwind.css | Đường dẫn tới file CSS toàn cục chứa khai báo biến màu | Trỏ sai thì component chép về không có màu |
| tailwind.baseColor | Dải màu xám nền tảng: slate, gray, zinc, neutral, stone | Đây là màu trung tính, không phải màu thương hiệu |
| tailwind.cssVariables | Dùng biến CSS hay viết thẳng lớp màu Tailwind | Nên để true, đó là điều kiện để bật chế độ tối và đổi chủ đề lúc chạy |
| aliases | Ánh xạ bí danh đường dẫn tới components, utils, ui, lib, hooks | Phải khớp với cấu hình paths trong tsconfig hoặc jsconfig, lệch là báo lỗi không tìm thấy module |
| iconLibrary | Bộ biểu tượng dùng trong component | Mặc định là lucide-react, đổi sang bộ khác cần sửa lại import trong file đã chép |
6. Đọc hiểu một component: cấu trúc của button
Đây là dạng rút gọn của file button, đủ để thấy CVA hoạt động thế nào:
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 disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-white hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 px-3 text-xs',
lg: 'h-10 px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: { variant: 'default', size: 'default' },
}
);
function Button({ className, variant, size, ...props }) {
return (
<button className={cn(buttonVariants({ variant, size, className }))} {...props} />
);
}
export { Button, buttonVariants };
Muốn thêm biến thể riêng cho thương hiệu, bạn chỉ cần thêm một dòng vào object variants. Không phải ghi đè CSS, không phải dùng dấu chấm than important, không phải tra cứu tên lớp nội bộ của thư viện. So sánh với việc ghi đè kiểu dáng của một thư viện đóng gói sẵn thì đây là khác biệt về bản chất chứ không phải về mức độ tiện lợi.
Biến buttonVariants được export ra ngoài có một công dụng quan trọng: gắn giao diện nút lên một thẻ liên kết mà vẫn giữ đúng ngữ nghĩa HTML, ví dụ <Link className={buttonVariants({ variant: 'outline' })}>. Đây là cách đúng thay cho việc bọc thẻ liên kết bên trong thẻ nút.
7. Hệ màu bằng biến CSS và chế độ tối
Toàn bộ màu nằm ở dạng biến CSS trong file globals, với một bản cho chế độ sáng và một bản cho chế độ tối. Cách đặt tên đi theo cặp nền và chữ, ví dụ primary đi kèm primary-foreground, để mọi component luôn có màu chữ tương phản đúng.
: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);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--border: oklch(1 0 0 / 10%);
}
Không gian màu OKLCH được dùng thay cho HSL vì độ sáng cảm nhận được phân bố đều hơn, nên khi bạn giảm độ sáng đồng loạt để dựng bảng màu tối thì các sắc độ không bị lệch nhau. Việc bật chế độ tối trong Next.js làm qua thư viện next-themes, chi tiết đầy đủ nằm ở bài thiết kế giao diện Dark Mode chuẩn UX với Tailwind CSS.
Với Tailwind CSS v4, cấu hình chuyển hẳn vào file CSS qua chỉ thị theme thay vì file tailwind.config, khiến bước cài đặt Shadcn UI cũng đổi theo. Bài Tailwind CSS v4 có gì mới nói kỹ phần này.
8. Ranh giới Server Component: cạm bẫy phổ biến nhất trong App Router
Đây là chỗ khiến nhiều dự án App Router mất điểm hiệu năng mà không hiểu vì sao. Mọi component có tương tác của Shadcn UI đều mang dòng use client ở đầu file, vì Radix Primitives cần hook và sự kiện phía trình duyệt.
Vấn đề nằm ở chỗ: khi một Server Component import và render một Client Component, phần bên trong nó truyền vào dưới dạng children vẫn được giữ ở phía máy chủ, nhưng nếu bạn import trực tiếp thì toàn bộ cây con sẽ bị kéo sang phía trình duyệt.
| Cách viết | Hệ quả |
|---|---|
| Trang là Client Component, chứa toàn bộ nội dung và vài nút bấm | Toàn bộ nội dung tĩnh bị gửi kèm mã JavaScript, dung lượng gói tăng vô ích |
| Trang là Server Component, chỉ bọc phần tương tác vào một Client Component nhỏ | Nội dung tĩnh được kết xuất ở máy chủ, chỉ phần nhỏ cần JavaScript mới được tải |
| Trang là Server Component, truyền nội dung tĩnh làm children cho Dialog | Nội dung vẫn được kết xuất phía máy chủ, đây là cách tối ưu nhất |
Nguyên tắc thực hành: đẩy ranh giới use client xuống càng sâu trong cây component càng tốt, và tận dụng cơ chế truyền children thay vì import trực tiếp. Chủ đề này liên quan mật thiết tới bài SEO cho React Server Components và kích thước RSC Payload.
9. Cập nhật component khi không có lệnh npm update
Đây là cái giá của mô hình sở hữu mã nguồn: khi tác giả sửa một lỗi trợ năng trong component Select, bạn sẽ không nhận được bản vá đó một cách tự động. Quy trình vận hành nên như sau:
- Không sửa trực tiếp file trong components/ui trừ khi thực sự cần. Muốn tùy biến riêng cho một màn hình thì bọc thêm một lớp component ở thư mục khác.
- Khi cần cập nhật, chạy lệnh add kèm cờ ghi đè cho đúng component đó, rồi dùng công cụ so sánh của Git để xem file thay đổi những gì trước khi chấp nhận.
- Ghim lại trong tài liệu nội bộ những file nào đã bị sửa tay và sửa chỗ nào, vì đó là các file sẽ gây xung đột khi ghi đè.
- Định kỳ mỗi quý rà lại các component quan trọng như Dialog, Select, Combobox vì đây là nhóm hay được vá lỗi trợ năng nhất.
Với dự án nhiều người, cách bền vững hơn là dựng registry nội bộ: chép các component đã tùy biến theo hệ thiết kế của công ty lên một địa chỉ riêng, rồi mọi dự án cài về bằng CLI qua URL đó. Lúc này Shadcn UI đóng vai trò cơ chế phân phối cho design system nội bộ chứ không còn là thư viện bên ngoài.
10. Bảng lỗi thường gặp khi mới cài
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Báo không tìm thấy module @/components/ui/button | Bí danh trong components.json lệch với paths trong tsconfig | Sửa cho hai file khớp nhau, khởi động lại máy chủ phát triển |
| Component hiện ra nhưng không có màu, trông như HTML thô | File CSS toàn cục chưa được import vào layout gốc, hoặc thiếu khối biến màu | Kiểm tra import trong app/layout và sự tồn tại của khối biến trong globals |
| Chế độ tối đổi được biến nhưng giao diện không đổi | Thiếu cấu hình darkMode theo lớp, hoặc lớp dark chưa được gắn lên thẻ html | Kiểm tra cấu hình Tailwind và thuộc tính suppressHydrationWarning trên thẻ html |
| Lỗi hydration mismatch ngay khi tải trang | Chủ đề sáng tối được đọc từ localStorage nên máy chủ và trình duyệt kết xuất khác nhau | Dùng next-themes đúng cách và không kết xuất phần phụ thuộc chủ đề trong lần vẽ đầu |
| Dialog mở lên bị nhảy layout, thanh cuộn biến mất gây giật | Radix khóa cuộn bằng cách đặt overflow hidden lên body | Bù chiều rộng thanh cuộn bằng biến scrollbar-gutter, xem thêm bài về CLS bên dưới |
| Chép về file .tsx trong khi dự án viết JavaScript | Trường tsx trong components.json đang để true | Đặt lại thành false rồi chép lại component |
| Gói JavaScript phình to bất thường | Đặt use client ở tầng quá cao trong cây component | Xem lại mục 8, đẩy ranh giới xuống sâu hơn |
Riêng hiện tượng giật layout khi mở hộp thoại, nó ăn thẳng vào điểm Cumulative Layout Shift trong bộ chỉ số Core Web Vitals, nên đừng bỏ qua.
11. Ghép Shadcn UI với phần còn lại của hệ sinh thái
| Nhu cầu | Thư viện đi kèm | Ghi chú |
|---|---|---|
| Biểu mẫu và kiểm tra dữ liệu | react-hook-form và zod | Component Form của Shadcn UI chỉ là lớp bọc mỏng quanh hai thư viện này, chi tiết ở bài React Hook Form kết hợp Zod |
| Bảng dữ liệu phức tạp | TanStack Table | Shadcn UI chỉ cung cấp phần khung bảng, logic sắp xếp lọc phân trang bạn tự lắp |
| Dữ liệu từ máy chủ | TanStack Query | Xem bài TanStack Query v5 và kỹ thuật Optimistic Updates |
| Trạng thái phía trình duyệt | Zustand | Xem bài Zustand vs Redux Toolkit |
| Thông báo dạng toast | sonner | Đây là thư viện được khuyến nghị hiện nay, nguyên tắc thiết kế thông báo xem tại bài về Toast Notification |
Nhìn bảng này sẽ thấy đặc điểm quan trọng: Shadcn UI cố tình không ôm hết mọi thứ. Nó chỉ lo phần trình bày và hành vi của từng khối giao diện, phần còn lại bạn tự chọn. Đó vừa là điểm mạnh về tính linh hoạt, vừa là gánh nặng quyết định với đội chưa có kinh nghiệm.
12. Khi nào KHÔNG nên dùng Shadcn UI
- Dự án quản trị nội bộ cần dựng trong hai tuần: khi yêu cầu là ba mươi màn hình danh sách với bộ lọc, phân trang, xuất Excel và cây thư mục, một thư viện trọn gói như Ant Design cho tốc độ vượt trội. Bạn không cần giao diện độc bản cho trang mà chỉ nhân viên nội bộ nhìn thấy.
- Đội chưa quen Tailwind CSS: mỗi component là một khối className dài, người chưa thạo Tailwind sẽ thấy mã nguồn khó đọc và dễ sửa hỏng.
- Cần các component nghiệp vụ nặng có sẵn: bộ chọn ngày theo lịch âm, bảng có ô gộp và cố định cột, cây kéo thả, trình soạn thảo văn bản giàu định dạng. Những thứ này Shadcn UI không có, bạn phải tự ghép từ thư viện khác.
- Dự án không dùng React: đây là mã nguồn React, các cổng chuyển sang Vue hay Svelte là dự án cộng đồng riêng biệt, mức độ hoàn thiện không tương đương.
- Tổ chức muốn mọi dự án bị ràng buộc vào một phiên bản giao diện duy nhất: mô hình chép mã nguồn khiến mỗi dự án trôi dạt theo hướng riêng, trừ khi bạn đủ kỷ luật dựng registry nội bộ như đã nói ở mục 9.
13. Danh sách kiểm tra trước khi đưa vào dự án thật
- Bí danh đường dẫn trong components.json và tsconfig đã khớp.
- Trường rsc đã bật nếu dùng App Router, trường tsx đã đúng ngôn ngữ dự án.
- Bảng màu thương hiệu đã được ghi vào biến CSS, không viết cứng mã màu trong từng component.
- Đã kiểm tra tương phản màu chữ và nền đạt tối thiểu tỉ lệ 4.5 trên 1 ở cả chế độ sáng và tối.
- Đã rà lại vị trí đặt use client trong cây component.
- Đã chạy công cụ phân tích gói để xem dung lượng JavaScript thực tế gửi tới trình duyệt.
- Đã ghi lại trong tài liệu nội bộ những component nào đã bị sửa tay.
- Đã kiểm tra thao tác bằng bàn phím trên Dialog, Select và Combobox.
Kết luận
Shadcn UI không phải một thư viện tốt hơn hay tệ hơn các thư viện khác, nó là một mô hình phân phối khác. Bạn đổi sự tiện lợi của việc nâng cấp tự động lấy quyền kiểm soát tuyệt đối mã nguồn giao diện. Với trang bán hàng, landing page và sản phẩm phần mềm dịch vụ cần bản sắc riêng cùng điểm tốc độ cao, đó là đánh đổi rất đáng giá.
Uwmee Software nhận thiết kế website Next.js trên nền Shadcn UI và Tailwind CSS với hệ thiết kế riêng theo thương hiệu. Đọc tiếp bài so sánh Shadcn UI với Ant Design v5 và bài so sánh tốc độ trang giữa Elementor, Divi và Tailwind CSS kết hợp Next.js.