· shadcn / radix-ui / react

shadcn/ui vs Radix UI primitives: khi nào dùng cái nào

shadcn/ui được xây trên Radix — điều đó thay đổi cả câu hỏi. Dùng shadcn cho team Tailwind; chọn thẳng Radix khi cần kiểm soát brand cho design system.

Bởi Ethan · Cập nhật 17 tháng 5, 2026

1.613 từ · 9 phút đọc

Dùng shadcn/ui nếu bạn đang dùng Tailwind và muốn có component accessible trong vòng chưa đầy một tiếng. Dùng thẳng Radix primitives nếu bạn đang xây design system với yêu cầu kiểm soát brand nghiêm ngặt, hoặc nếu team bạn đang dùng CSS-in-JS. Đó là hai bài toán khác nhau. Sự nhầm lẫn thường đến từ việc không biết rằng cả hai dùng chung một lớp code bên dưới.

Bài này dành cho ai

Các developer React đã thấy cả hai cái tên, chưa rõ mối quan hệ giữa chúng, và cần câu trả lời thẳng thắn trước khi chọn. Bài này đề cập đến [email protected] (phát hành 2026-05-05) và [email protected] (gói thống nhất, phát hành 2025-08-13). Cả hai đều chỉ dành cho React — nếu bạn đang cân nhắc framework, hãy xem bài React vs Vue của chúng tôi trước.

Mỗi thứ là gì

shadcn/ui

Tài liệu của họ nói thẳng: “Đây không phải là một component library. Đây là cách bạn xây component library của riêng mình.”

Khi bạn chạy npx shadcn add dialog, bạn không cài package. Bạn đang copy một file .tsx vào repo. File đó sống trong codebase của bạn, bạn sở hữu nó, bạn chỉnh sửa trực tiếp. Không có phiên bản component upstream nào để upgrade.

Tính đến tháng 5/2026, shadcn/ui đã có 114.504 GitHub stars và hỗ trợ Next.js, Vite, TanStack Start, React Router, Astro, và Laravel.

Radix UI primitives

Radix cung cấp component không có style sẵn, nhưng đầy đủ accessibility dưới dạng npm package. Bạn cài @radix-ui/react-dialog, ghép các sub-component lại, rồi tự mang CSS của mình vào. Không có gì được style trước. Không có gì được quyết định thay cho bạn.

import { Popover } from "radix-ui";

<Popover.Root>
  <Popover.Trigger>More info</Popover.Trigger>
  <Popover.Portal>
    <Popover.Content>
      Some more info…
      <Popover.Arrow />
    </Popover.Content>
  </Popover.Portal>
</Popover.Root>

Đây là một popover hoàn chỉnh, đầy đủ accessibility. Phần style là việc của bạn hoàn toàn.

Mối quan hệ giữa hai thứ

shadcn/ui là một lớp nằm trên Radix. Stack trông như thế này:

Code ứng dụng của bạn

shadcn/ui components (source nằm trong repo của bạn)

Radix UI primitives (cài qua npm: @radix-ui/react-*)

Tailwind CSS v4 (styling qua CSS variables)

Khi bạn thêm một Dialog từ shadcn, nó cài @radix-ui/react-dialog từ npm và thêm một wrapper component vào source tree của bạn. Package Radix làm phần accessibility — keyboard trapping, ARIA attributes, focus management. shadcn thêm lớp visual và các class Tailwind đã được chọn sẵn.

Điều này quan trọng khi nói đến bundle size: shadcn không thêm runtime overhead nào ngoài Radix. Cả hai cách đều cài cùng một package Radix. Sự khác biệt chỉ là vài trăm byte từ file .tsx được copy và chuỗi class Tailwind.

Mức độ tùy chỉnh

shadcn xử lý ra sao

shadcn có hai lớp. Lớp đầu là hệ thống CSS variable token — các semantic token như --primary, --background, --radius mà components tham chiếu. Ghi đè chúng trong global CSS, toàn bộ UI thay đổi theo. Nó dùng oklch() trong Tailwind v4, cho phép làm việc với dải màu rộng hơn.

Lớp thứ hai là chính source code. Vì component nằm trong repo của bạn, không có API nào cần phải đi vòng. Bạn muốn thay đổi hover state của button? Sửa thẳng vào button. Không cần prop drilling. Không phải học thêm theme override DSL.

Đánh đổi ở đây: bạn bị gắn với Tailwind dù muốn hay không. Nếu bạn đang so sánh phương án styling, bài Tailwind vs CSS Modules của chúng tôi phân tích chi tiết về chi phí dài hạn.

Radix xử lý ra sao

Radix không có style theo thiết kế. Bạn gắn class từ bất kỳ hệ thống nào bạn dùng — CSS Modules, styled-components, Emotion, vanilla CSS, Tailwind. Prop asChild cho phép bạn render element của mình thay vì element mặc định của Radix:

<Button asChild>
  <a href="/login">Log in</a>
</Button>

Chính khả năng composability này là lý do các team xây design system thực sự thích Radix hơn. Bạn có behavior (focus, ARIA, keyboard) mà không có bất kỳ visual default nào cần phải gỡ bỏ.

Ảnh hưởng đến bundle size

Số liệu từ Bundlephobia, truy xuất ngày 2026-05-17 (tất cả package đều tree-shakeable, ESM entry point):

ComponentRadix packageGzipped
Dialog@radix-ui/react-dialog 1.1.1510.6 kB
Tabs@radix-ui/react-tabs 1.1.134.9 kB
Select@radix-ui/react-select 2.2.623.7 kB
Dropdown Menu@radix-ui/react-dropdown-menu 2.1.1624.3 kB

Kích thước lớn hơn của Select và Dropdown Menu đến từ Floating UI (engine định vị), thứ thêm khoảng 38 kB đã minify vào các package này.

Dùng shadcn không thay đổi những con số này. Dù cách nào bạn cũng cài cùng Radix package. Số lượt tải hằng tuần của @radix-ui/react-dialog — 51 triệu lần/tuần — phản ánh điều này: phần lớn trong số đó là các project shadcn kéo Radix vào dưới dạng transitive dependency.

Khi nào chọn shadcn/ui

  • Team bạn đã dùng Tailwind — không có gì phải lo về tích hợp.
  • Bạn cần UI hoạt động được nhanh và không có designer sẽ xây dựng hệ thống token đầy đủ.
  • Bạn muốn sở hữu code: component shadcn trong repo của bạn có thể được vá lỗi Radix trực tiếp, không cần chờ upstream release.
  • Bạn muốn tận dụng cộng đồng: nhiều extension, ví dụ mẫu, và câu trả lời trên Stack Overflow.

Khi nào dùng thẳng Radix primitives

  • Bạn đang xây design system nội bộ với yêu cầu kiểm soát brand nghiêm ngặt. Vercel và Linear đều chọn Radix trực tiếp, không qua shadcn. Vercel cần sở hữu toàn bộ visual token. Linear xây hệ thống Orbiter design system trên Radix với styled-components.
  • Stack của bạn là CSS-in-JS hoặc CSS Modules — gắn chặt với Tailwind sẽ đòi hỏi một cuộc migration song song.
  • Bạn đang xây white-label hoặc multi-tenant UI mà không thể có bất kỳ aesthetic default nào lọt qua.
  • Bạn cần kiểm soát bundle size chi tiết theo từng primitive.

Đội engineering của Vercel nói thẳng: “Chúng tôi có thể tập trung vào việc xây dựng trải nghiệm người dùng tốt trên nền Radix Primitives.” Stack trước đây của họ — component tùy chỉnh + Reach UI + React Spectrum — đã được hợp nhất sang Radix vì họ cần một nền tảng accessible nhất quán mà design team có thể style từ đầu.

Tình trạng bảo trì của Radix

Đây là thông tin bạn nên có trước khi chọn dứt khoát.

Nhịp độ bảo trì của Radix đã chậm lại kể từ khi WorkOS mua lại. Nhiều bug tồn tại lâu năm vẫn chưa được xử lý. Một GitHub Discussion trên repo shadcn — 55 reactions — kêu gọi chuyển từ Radix sang Base UI của MUI. Một người comment: “Cứ hai tuần một lần tôi lại gặp một bug trong radix-ui đã được mở từ nhiều năm trước.”

Vấn đề cụ thể: HoverCard với số lượng lớn (50–100 instance) có thể gây ra lỗi “Maximum update depth exceeded” do thiếu dependency trong useEffect bên trong @radix-ui/react-popper. Dạng internal setState storm này xuất hiện ở nhiều issue đang mở.

Câu trả lời của shadcn: từ cuối 2025, các project mới có thể chọn Base UI (thư viện headless primitive của MUI) làm engine nền thay vì Radix, trong khi giữ nguyên component API. Nếu bạn đang bắt đầu project shadcn mới và lo ngại về hướng đi của Radix, lựa chọn đó đã có sẵn.

Nếu bạn dùng Radix primitives trực tiếp, hãy theo dõi issue tracker. Với hầu hết component (Tabs, Accordion, Dialog ở quy mô bình thường) thư viện vẫn hoạt động tốt. Với những gì liên quan đến floating positioning ở quy mô lớn, hãy test trên workload cụ thể của bạn.

Kết luận

Chọn shadcn/ui nếu bạn là team Tailwind muốn có component accessible mà không cần tự xây design system từ đầu. Mặc định của shadcn đủ tốt, model tùy chỉnh rõ ràng, và Base UI support cho bạn một lối thoát khỏi Radix khi cần.

Chọn Radix primitives trực tiếp nếu bạn đang xây design system nội bộ, nếu việc gắn chặt với Tailwind là vấn đề, hoặc nếu bạn cần kiểm soát toàn bộ visual token cho white-label. Bạn sẽ phải tự làm phần styling — đó chính là mục đích.

Những project bị nhầm lẫn thường là những project chọn Radix primitives mà nghĩ sẽ có component có style sẵn, hoặc chọn shadcn mà nghĩ có thể tách khỏi Tailwind. Cả hai đều không thể. Cả hai lựa chọn đều tốt; chỉ là tốt cho những thứ khác nhau.

Lưu ý

Bài viết này không có affiliate link — không có chương trình affiliate của Vercel hay component library nào khả dụng tại thời điểm viết bài. Tất cả số liệu bundle đều lấy từ public API của Bundlephobia tính đến 2026-05-17. Tích hợp shadcn + Base UI đang trong quá trình phát triển tích cực vào cuối 2025; hãy xem tài liệu hiện tại trước khi áp dụng vào production.

Tham khảo