Shadcn UI là gì? Hướng dẫn cài đặt và làm chủ Shadcn UI trong Next.js App Router

2026-08-11 16 phút đọc 0 lượt xemBởi Uwmee Software

Shadcn UI là bộ mã nguồn component viết sẵn bằng Radix UI PrimitivesTailwind 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, ChakraLà một bộ sưu tập mã nguồn (registry) kèm công cụ chép file
Import component từ node_modulesImport từ chính thư mục dự án, ví dụ components/ui/button
Nâng cấp bằng npm updateNâ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ố địnhSửa trực tiếp file, không cần override CSS hay ghi đè token
Xóa đi thì gãy toàn dự ánXó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ớpThư việnChịu trách nhiệm
Hành vi và trợ năngRadix UI PrimitivesBẫ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ệnTailwind CSSToà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ớpclsx 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ĩaLưu ý khi sửa
styleBộ style dùng chung cho mọi componentChỉ có tác dụng lúc chép file mới, đổi về sau không hồi tố các file đã có
rscCó 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
tsxSinh file TypeScript hay JavaScriptDự á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àuTrỏ sai thì component chép về không có màu
tailwind.baseColorDả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.cssVariablesDùng biến CSS hay viết thẳng lớp màu TailwindNê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, hooksPhả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
iconLibraryBộ biểu tượng dùng trong componentMặ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ếtHệ quả
Trang là Client Component, chứa toàn bộ nội dung và vài nút bấmToà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 DialogNộ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:

  1. 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.
  2. 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.
  3. 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 đè.
  4. Đị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ứngNguyên nhânCách xử lý
Báo không tìm thấy module @/components/ui/buttonBí danh trong components.json lệch với paths trong tsconfigSử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àuKiể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 đổiThiếu cấu hình darkMode theo lớp, hoặc lớp dark chưa được gắn lên thẻ htmlKiể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 trangChủ đề sáng tối được đọc từ localStorage nên máy chủ và trình duyệt kết xuất khác nhauDù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ậtRadix khóa cuộn bằng cách đặt overflow hidden lên bodyBù 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 JavaScriptTrườ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 componentXem 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ầuThư viện đi kèmGhi chú
Biểu mẫu và kiểm tra dữ liệureact-hook-form và zodComponent 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ạpTanStack TableShadcn 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 QueryXem bài TanStack Query v5 và kỹ thuật Optimistic Updates
Trạng thái phía trình duyệtZustandXem bài Zustand vs Redux Toolkit
Thông báo dạng toastsonnerĐâ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

  1. Bí danh đường dẫn trong components.json và tsconfig đã khớp.
  2. Trường rsc đã bật nếu dùng App Router, trường tsx đã đúng ngôn ngữ dự án.
  3. 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.
  4. Đã 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.
  5. Đã rà lại vị trí đặt use client trong cây component.
  6. Đã 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.
  7. Đã ghi lại trong tài liệu nội bộ những component nào đã bị sửa tay.
  8. Đã 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.

Câu hỏi thường gặp

Bạn cần giải pháp phần mềm chuyên nghiệp?

Uwmee Software cung cấp dịch vụ 12 tester Google Play & thiết kế website cao cấp giúp nâng tầm thương hiệu của bạn.