jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Tailwind, Radix & shadcn/ui · Part 1 — The Mental Model & Setup (Tailwind v4)

Start from zero: why utility-first CSS exists, the one idea that makes Tailwind click, and a complete v4 setup with Vite — install, @import, @theme, and your first composed component. With a live playground.

This is Part 1 of a 12-part series that takes you from “I keep copy-pasting Tailwind classes I don’t understand” to fluently building accessible, themeable component systems with Tailwind CSS v4 + Radix UI + shadcn/ui {Đây là Phần 1 của series 12 bài đưa bạn từ “cứ copy-paste class Tailwind mà không hiểu” đến thành thạo dựng hệ component dễ tiếp cận, dễ theme với Tailwind CSS v4 + Radix UI + shadcn/ui}. Every part ships real config (setup → demo) and a live, interactive demo — and ends with exercises {Mỗi phần đều có config thật (setup → demo) và một demo tương tác trực tiếp — kết thúc bằng bài tập}.

These three tools form one stack {Ba công cụ này hợp thành một stack}: Tailwind styles, Radix gives you headless accessible behavior, and shadcn/ui is the glue — pre-built components you own, styled with Tailwind, powered by Radix {Tailwind lo style, Radix lo hành vi headless dễ tiếp cận, và shadcn/ui là chất keo — component dựng sẵn mà bạn sở hữu, style bằng Tailwind, chạy trên Radix}. We start at the foundation: Tailwind {Ta bắt đầu từ nền móng: Tailwind}.


1. The problem utility-first solves {Vấn đề mà utility-first giải quyết}

Traditional CSS asks you to invent a name for everything {CSS truyền thống bắt bạn nghĩ ra tên cho mọi thứ}. You write .card, .card__title, .card--featured, then jump between an HTML file and a CSS file to keep them in sync {Bạn viết .card, .card__title, .card--featured, rồi nhảy qua lại giữa file HTML và file CSS để giữ chúng đồng bộ}. Names drift, files grow forever (CSS is append-only in practice), and “is this class still used?” becomes unanswerable {Tên bị lệch, file phình mãi (CSS thực tế chỉ thêm chứ hiếm khi xóa), và câu hỏi “class này còn dùng không?” trở nên không thể trả lời}.

/* The traditional way — naming + a separate file + context switching */
.btn-primary {
  padding: 0.5rem 1rem;
  border-radius: 0.375rem;
  background: #6366f1;
  color: white;
  font-weight: 600;
}
<button class="btn-primary">Save</button>

Tailwind flips it {Tailwind lật ngược lại}: instead of naming a component then defining its styles elsewhere, you apply small, single-purpose utility classes directly in markup {thay vì đặt tên cho một component rồi định nghĩa style ở nơi khác, bạn áp các utility class nhỏ, đơn-nhiệm ngay trong markup}.

<!-- The Tailwind way — design in your markup, no naming, no CSS file -->
<button class="px-4 py-2 rounded-md bg-indigo-500 text-white font-semibold">
  Save
</button>

Each class is one CSS declaration {Mỗi class là một khai báo CSS}: px-4padding-left/right: 1rem, rounded-mdborder-radius: 0.375rem, and so on {và cứ thế}. That’s the whole idea {Đó là toàn bộ ý tưởng}.


2. The one idea that makes it click {Một ý tưởng làm mọi thứ “thông”}

A utility class is a name for a single, fixed CSS declaration. {Một utility class là cái tên cho một khai báo CSS đơn lẻ, cố định.}

The values aren’t random — they come from a design system scale {Các giá trị không tùy tiện — chúng đến từ một thang design system}. Spacing steps (1 = 0.25rem, 2 = 0.5rem, 4 = 1rem), a color palette (indigo-500, slate-200), a radius scale, a shadow scale {Bước spacing, bảng màu, thang radius, thang shadow}. So your UI is consistent by construction — you can only pick from the scale {Nhờ vậy UI của bạn nhất quán theo thiết kế — bạn chỉ chọn được trong thang}.

Play with it below — toggle utilities and watch the class string and the equivalent CSS Tailwind generates {Thử ngay bên dưới — bật/tắt utility và xem chuỗi class cùng CSS tương đương mà Tailwind sinh ra}:

The two common objections, answered up front {Hai phản đối thường gặp, trả lời luôn}:

  • “The markup is ugly / too long.” You stop reading the classes after a week, the same way you stopped reading display: flex {“Markup xấu / dài quá.” Sau một tuần bạn ngừng “đọc” các class, y như đã ngừng đọc display: flex}. And repetition is solved by components, not by naming CSS — Part 5 {Lặp lại được giải quyết bằng component, không phải đặt tên CSS — Phần 5}.
  • “Won’t the CSS be huge?” No — Tailwind scans your files and generates only the classes you actually use {“CSS sẽ to lắm?” Không — Tailwind quét file và chỉ sinh ra những class bạn thực sự dùng}. A typical production build is a few kB {Một build production điển hình chỉ vài kB}.

3. Setup — Tailwind v4 with Vite {Cài đặt — Tailwind v4 với Vite}

We target Tailwind v4, which is a big simplification over v3 {Ta nhắm Tailwind v4, đơn giản hơn hẳn v3}. The most common setup for React work is Vite {Setup phổ biến nhất cho React là Vite}.

Step 1 — install {Bước 1 — cài}:

npm install tailwindcss @tailwindcss/vite

Step 2 — add the Vite plugin {Bước 2 — thêm plugin Vite}. In v4 there is no PostCSS config and no tailwind.config.js required — the plugin does it all {Ở v4 không cần config PostCSS và không bắt buộc tailwind.config.js — plugin lo hết}:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

Step 3 — import Tailwind in your CSS {Bước 3 — import Tailwind trong CSS}. This single line replaces the old @tailwind base; @tailwind components; @tailwind utilities; trio {Một dòng này thay cho bộ ba @tailwind base/components/utilities cũ}:

/* src/index.css */
@import "tailwindcss";

Step 4 — make sure that CSS is loaded once {Bước 4 — đảm bảo CSS được nạp một lần}, at your app entry {tại entry của app}:

// src/main.tsx
import './index.css';

That’s it — npm run dev and every utility class is available {Xong — npm run dev và mọi utility class đã sẵn sàng}.

v3 vs v4 in one breath {v3 vs v4 trong một hơi}: v3 used a JS config file (tailwind.config.js) + PostCSS + the three @tailwind directives {v3 dùng file config JS + PostCSS + ba directive @tailwind}. v4 uses a Vite/PostCSS plugin, a single @import "tailwindcss", and moves configuration into CSS via @theme (Part 3) {v4 dùng plugin Vite/PostCSS, một @import "tailwindcss", và đưa cấu hình vào trong CSS qua @theme (Phần 3)}. If you land in a v3 codebase, the utility classes are nearly identical — only the setup differs {Nếu bạn vào codebase v3, các utility class gần như giống hệt — chỉ khác phần setup}.

Editor setup — do this once {Cài cho editor — làm một lần}

Install the Tailwind CSS IntelliSense VS Code extension {Cài extension Tailwind CSS IntelliSense cho VS Code}. You get autocomplete for every class, hover-to-see-the-CSS, and color previews {Bạn có autocomplete cho mọi class, hover để xem CSS, và preview màu}. This single tool is what turns “memorizing classes” into “discovering them as you type” {Chính công cụ này biến “học thuộc class” thành “khám phá khi đang gõ”}.


4. Your first composed component {Component đầu tiên do bạn ghép}

Let’s read a real card line by line — every class is a declaration you now recognize {Đọc một card thật từng dòng — mỗi class là một khai báo bạn đã nhận ra}:

<div class="max-w-sm rounded-xl bg-white p-6 shadow-md">
  <h3 class="text-lg font-semibold text-slate-900">Deploy faster</h3>
  <p class="mt-2 text-sm text-slate-600">
    Ship your UI without writing a single line of custom CSS.
  </p>
  <button class="mt-4 rounded-md bg-indigo-500 px-4 py-2 text-sm font-medium text-white">
    Get started
  </button>
</div>

Reading it as CSS {Đọc nó dưới dạng CSS}: a max-width of 24rem, border-radius: 0.75rem, white background, 1.5rem padding, a medium shadow {max-width 24rem, border-radius: 0.75rem, nền trắng, padding 1.5rem, shadow vừa}. The title is 1.125rem, semibold, near-black {Tiêu đề 1.125rem, semibold, gần đen}. mt-2 / mt-4 are top-margins from the same spacing scale, which is why the rhythm looks right without you measuring anything {mt-2 / mt-4 là margin-top từ cùng thang spacing, nên nhịp nhìn cân mà không cần đo gì}.

That consistency — every value drawn from one shared scale — is the real payoff of utility-first, and the foundation everything else in this series builds on {Sự nhất quán đó — mọi giá trị rút từ một thang chung — mới là phần thưởng thật của utility-first, và là nền cho mọi thứ còn lại trong series}.


5. Exercises {Bài tập}

Do them in a fresh Vite + React project with the setup above {Làm trong một project Vite + React mới với setup ở trên}.

1. Recreate the “Save” button (px-4 py-2 rounded-md bg-indigo-500 text-white font-semibold) from scratch, then change only its color to emerald and its radius to fully rounded {Dựng lại nút “Save” từ đầu, rồi chỉ đổi màu sang emerald và bo tròn hoàn toàn}.

Solution {Lời giải}
<button class="px-4 py-2 rounded-full bg-emerald-500 text-white font-semibold">
  Save
</button>

Only bg-indigo-500bg-emerald-500 and rounded-mdrounded-full change {Chỉ đổi bg-indigo-500bg-emerald-500rounded-mdrounded-full}.

2. Without looking it up, write the CSS that p-8 generates {Không tra cứu, viết CSS mà p-8 sinh ra}.

Solution {Lời giải}

padding: 2rem; — the spacing scale is step * 0.25rem, so 8 * 0.25 = 2rem {thang spacing là bước * 0.25rem, nên 8 * 0.25 = 2rem}.

3. Build a “tag” pill: small text, horizontal padding 0.5rem, vertical padding 0.125rem, fully rounded, light gray background, dark gray text {Dựng một “tag” pill: chữ nhỏ, padding ngang 0.5rem, padding dọc 0.125rem, bo tròn, nền xám nhạt, chữ xám đậm}.

Solution {Lời giải}
<span class="px-2 py-0.5 text-xs rounded-full bg-slate-200 text-slate-700">New</span>

Stretch {Nâng cao}: open the playground above and reproduce exactly this CSS by toggling chips: padding: 1.5rem; border-radius: 9999px; box-shadow: 0 20px 25px -5px ...; font-weight: 700; {mở playground và tái tạo đúng CSS này bằng cách bật chip}.


Key takeaways {Điểm chính}

  • Utility-first = design in your markup with tiny single-purpose classes, instead of naming components and editing a separate stylesheet {Utility-first = thiết kế ngay trong markup bằng các class nhỏ đơn-nhiệm, thay vì đặt tên component và sửa stylesheet riêng}.
  • A utility = one CSS declaration, and its values come from a shared design scale → consistency by construction {Một utility = một khai báo CSS, giá trị đến từ thang design chung → nhất quán theo thiết kế}.
  • v4 setup is tiny: @tailwindcss/vite plugin + one @import "tailwindcss". No PostCSS, no required JS config {Setup v4 cực gọn: plugin @tailwindcss/vite + một @import "tailwindcss". Không PostCSS, không bắt buộc config JS}.
  • Install Tailwind IntelliSense — it turns memorization into discovery {Cài Tailwind IntelliSense — biến học thuộc thành khám phá}.

Next up {Tiếp theo}

Part 2 — Core utilities & layout: the utilities you’ll use every single day — spacing, sizing, typography, colors, and the two layout engines (Flexbox & Grid) the Tailwind way — plus the responsive system that makes one component adapt to every screen {Phần 2 — Utility cốt lõi & layout: những utility bạn dùng hằng ngày — spacing, sizing, typography, màu, và hai cỗ máy layout (Flexbox & Grid) theo kiểu Tailwind — cùng hệ responsive giúp một component thích ứng mọi màn hình}.