Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

Tailwind CSS 2026: Cách Build UI Nhanh, Đẹp Và Dễ Scale Cho Dự Án Thật

Ếch Trendy
Ếch Trendy

5 giờ tới · 6 phút đọc

Tại sao Tailwind vẫn là lựa chọn hàng đầu năm 2026?

Mình nhớ lần đầu thử Tailwind, nhìn vào đống class như flex items-center justify-between gap-4 rounded-xl bg-white shadow-md p-4 mà muốn tắt máy luôn. Cảm giác như ai đó đổ cả file CSS vào HTML. Nhưng sau 2 tuần build thật, mình không quay lại cách cũ nữa.

Lý do đơn giản: utility-first không phải là viết inline style. Nó là một hệ thống có constraint. Bạn không nghĩ "margin là bao nhiêu pixel" mà nghĩ "gap-4 hay gap-6" - tức là chọn từ design tokens, không bịa số.

347340 Utility-first = chọn từ hệ thống, không bịa số - đây là điểm khác biệt cốt lõi

Tailwind hiện được dùng trong hàng triệu dự án JavaScript trên npm, và con số này tiếp tục tăng mỗi quý. Lý do không chỉ là "nhanh" hay "đẹp" mà là tốc độ từ prototype đến production giảm đáng kể khi cả team nói chung một ngôn ngữ design.

Nếu bạn đang mới bắt đầu với HTML/CSS và chưa tự tin về nền tảng, khóa HTML CSS từ Zero đến Hero ở F8 sẽ giúp bạn vững trước khi vào Tailwind. Còn nếu đã có nền tảng rồi, bắt đầu thôi.


Setup Tailwind đúng cách từ đầu

Cài Tailwind không khó, nhưng setup sai ngay từ đầu sẽ đau về sau. Đây là workflow mình dùng cho dự án React/Next.js production.

# Cài Tailwind v4 (standalone, không cần PostCSS riêng)
npm install tailwindcss @tailwindcss/vite
// vite.config.ts - thêm plugin Tailwind
import tailwindcss from '@tailwindcss/vite'

export default {
  plugins: [
    tailwindcss(),
  ],
}
/* main.css - import một dòng duy nhất */
@import "tailwindcss";

/* Định nghĩa design tokens ngay tại đây */
@theme {
  --color-brand: #6366f1;       /* Màu chính của dự án */
  --color-brand-dark: #4f46e5;  /* Hover state */
  --font-sans: 'Inter', sans-serif;
  --radius-card: 0.75rem;
}

347341 Ba file, một lần setup - đừng để config phức tạp hơn cần thiết

Tailwind v4 thay đổi lớn so với v3: không còn tailwind.config.js cho hầu hết use case. Mọi customization giờ nằm trong CSS file qua @theme. Cái này mình thấy sạch hơn nhiều vì config và code sống ở cùng một nơi.

Một điểm hay: v4 mặc định scan toàn bộ project, không cần khai báo content paths nữa. Ít friction, ship nhanh hơn.


Responsive design patterns thực chiến

Bạn đã từng viết media query rồi không biết nó đang override cái gì? Với Tailwind, responsive được baked-in ngay trong class name.

Quy tắc cơ bản: mobile-first. Class không có prefix = mobile. Prefix md:, lg:, xl: = apply từ breakpoint đó trở lên.

// Layout thay đổi từ 1 cột (mobile) → 3 cột (desktop)
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  <Card />
  <Card />
  <Card />
</div>

// Text size responsive
<h1 className="text-2xl md:text-4xl lg:text-5xl font-bold">
  Heading tự động scale
</h1>

// Ẩn/hiện theo breakpoint
<nav className="hidden md:flex items-center gap-4">
  {/* Desktop nav */}
</nav>
<button className="md:hidden p-2">
  {/* Mobile hamburger */}
</button>

347342 Mobile-first: viết ít hơn, override ít hơn - layout tự thích nghi

Pattern mình hay dùng nhất là container query - thay vì responsive theo viewport, component tự responsive theo container của nó:

// Tailwind v4 hỗ trợ container queries built-in
<div className="@container">
  <div className="grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3 gap-4">
    <Card />
  </div>
</div>

Cái này đặc biệt hữu ích khi cùng một component xuất hiện ở sidebar (nhỏ) và main content (rộng) - không cần viết 2 variant khác nhau.


Component hóa với shadcn/ui và Headless UI

Viết Tailwind thuần thì ổn, nhưng build cả design system từ đầu mất thời gian. Đây là lúc cần component library - nhưng chọn loại nào?

Mình chia ra 2 trường hợp:

shadcn/ui - khi bạn muốn copy component vào project, tự sở hữu và tùy chỉnh thoải mái:

# Init shadcn trong project
npx shadcn@latest init

# Thêm từng component cần dùng
npx shadcn@latest add button dialog data-table

Components được copy thẳng vào components/ui/ của bạn. Muốn đổi màu, đổi border-radius, thêm animation? Sửa trực tiếp, không cần override hay hack prop.

Headless UI - khi bạn cần behavior (accessibility, keyboard nav) mà không muốn bị ràng buộc style:

import { Dialog } from '@headlessui/react'

// Logic và accessibility được xử lý bởi Headless UI
// Style hoàn toàn do bạn quyết định bằng Tailwind
<Dialog.Panel className="mx-auto max-w-sm rounded-2xl bg-white p-6 shadow-xl">
  <Dialog.Title className="text-lg font-semibold text-gray-900">
    Xác nhận hành động
  </Dialog.Title>
</Dialog.Panel>

347343 shadcn/ui cho speed, Headless UI cho control - chọn theo nhu cầu dự án

Trong thực tế mình hay kết hợp cả hai: dùng shadcn cho những component phổ biến (Button, Input, Toast), dùng Headless UI cho những component đặc thù cần custom nặng.


Xây design system không bị chaos

Đây là phần mà hầu hết team bỏ qua lúc đầu, rồi sau 3 tháng nhìn lại codebase muốn refactor toàn bộ.

Vấn đề kinh điển: class name chaos. Button ở một chỗ dùng rounded-lg, chỗ khác rounded-md, chỗ khác rounded-xl. Màu primary thì bg-indigo-500 ở component này, bg-indigo-600 ở component kia. Không nhất quán, không có nguồn sự thật duy nhất.

Cách fix: design tokens trong @theme + component abstraction.

/* tokens.css - nguồn sự thật duy nhất */
@theme {
  /* Màu sắc - đặt tên theo semantic, không theo màu */
  --color-primary: #6366f1;
  --color-primary-hover: #4f46e5;
  --color-destructive: #ef4444;
  --color-surface: #ffffff;
  --color-surface-muted: #f8fafc;

  /* Typography */
  --font-sans: 'Inter', system-ui, sans-serif;
  --text-base: 1rem;
  --leading-normal: 1.5;

  /* Spacing scale */
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;

  /* Border radius */
  --radius-sm: 0.375rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
}
// Button.tsx - component abstraction tập trung style
const variants = {
  primary: 'bg-primary text-white hover:bg-primary-hover',
  secondary: 'bg-surface border border-slate-200 hover:bg-surface-muted',
  destructive: 'bg-destructive text-white hover:opacity-90',
}

export function Button({ variant = 'primary', size = 'md', children, ...props }) {
  return (
    <button
      className={cn(
        // Base styles - không bao giờ thay đổi
        'inline-flex items-center justify-center font-medium transition-colors',
        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
        // Size variants
        size === 'sm' && 'h-8 px-3 text-sm rounded-sm',
        size === 'md' && 'h-10 px-4 text-base rounded-md',
        size === 'lg' && 'h-12 px-6 text-lg rounded-lg',
        // Color variants
        variants[variant],
      )}
      {...props}
    >
      {children}
    </button>
  )
}

347344 Design tokens = một nơi thay đổi, ảnh hưởng toàn bộ dự án

Lợi ích ngay lập tức: khi designer muốn đổi màu primary, bạn chỉ sửa --color-primary trong @theme - toàn bộ dự án cập nhật. Không cần grep và replace hàng trăm file.

Một tool đáng dùng là clsx hoặc cn (kết hợp clsx + tailwind-merge) để merge class name an toàn - tránh conflict khi class override nhau.


Dark mode đúng cách

Dark mode bằng Tailwind đơn giản hơn bạn nghĩ - nhưng có một vài chỗ dễ làm sai.

/* Cách 1: Dùng CSS variables - khuyến nghị cho production */
@layer base {
  :root {
    --bg-base: 255 255 255;
    --text-base: 15 23 42;
    --border: 226 232 240;
  }

  .dark {
    --bg-base: 15 23 42;
    --text-base: 248 250 252;
    --border: 51 65 85;
  }
}
// Toggle dark mode bằng class trên html element
function ThemeToggle() {
  const [isDark, setIsDark] = useState(false)

  const toggle = () => {
    // Thêm/xóa class 'dark' trên <html>
    document.documentElement.classList.toggle('dark')
    setIsDark(prev => !prev)
  }

  return (
    <button onClick={toggle} className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800">
      {isDark ? <SunIcon /> : <MoonIcon />}
    </button>
  )
}

// Sử dụng trong component
<div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-50">
  <p className="text-slate-600 dark:text-slate-400">Nội dung text</p>
</div>

347345 CSS variables + class strategy: đổi theme không flash, không giật

Lỗi hay gặp nhất: hardcode màu thay vì dùng semantic token. bg-white không tự chuyển sang dark. Phải viết bg-white dark:bg-slate-900 hoặc tốt hơn là dùng CSS variable như trên để không phải viết 2 class ở khắp nơi.

Nếu dùng Next.js, dùng next-themes để handle system preference, localStorage persist và tránh flash khi reload.