Đ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 4.0: Arbitrary Values và OKLCH Colors cho Responsive Design siêu linh hoạt

Ếch Trendy
Ếch Trendy

5 tháng trước · 8 phút đọc

Tailwind CSS 4.0 thay đổi gì so với v3?

Mình nhớ lần đầu đọc release notes của Tailwind CSS 4.0 - cảm giác giống như họ viết lại từ đầu vậy. Không phải vì thêm vài class mới, mà vì triết lý cốt lõi thay đổi hoàn toàn: CSS-first thay vì JS-first.

Trong v3, cấu hình nằm hết trong tailwind.config.js. Muốn thêm màu? Vào JS. Muốn custom spacing? Vào JS. Muốn extend theme? Vào JS. Mọi thứ đều phải qua một file JS trung gian trước khi ra CSS. v4 đảo ngược hoàn toàn điều đó.

343580 CSS-first loại bỏ JS trung gian - config nằm thẳng trong CSS

Thay vì tailwind.config.js, bạn dùng directive @theme ngay trong file CSS:

/* app.css */
@import "tailwindcss";

@theme {
  --color-primary: oklch(60% 0.15 260);
  --color-accent: oklch(75% 0.18 140);
  --spacing-18: 4.5rem;
  --font-display: 'Be Vietnam Pro', sans-serif;
}

Tailwind tự động generate CSS variables từ @theme, sau đó dùng được ngay trong toàn bộ utility classes. Không cần resolveConfig, không cần import JS theme vào component.

Một thay đổi nhỏ nhưng có impact lớn: default scale của v4 được mở rộng đáng kể. Trước đây p-7 không tồn tại (v3 nhảy từ p-6 lên p-8), bạn phải viết p-[28px]. Giờ p-7, p-9, gap-7, m-9 đều có sẵn - {{fact:fact_0}} giá trị spacing mới được thêm vào default scale.

Kết quả? Arbitrary values vẫn còn đó, nhưng bạn cần dùng ít hơn nhiều cho spacing thông thường.


Arbitrary values trong v4: mạnh hơn, ít đau đầu hơn

Arbitrary values [...] không phải tính năng mới - v3 đã có. Nhưng v4 nâng nó lên một tầm khác.

Vấn đề cũ với v3: dùng calc() hoặc var() trong arbitrary values thường cần escape ký tự hoặc viết khá awkward:

<!-- v3: cần escape dấu cách bằng underscore -->
<div class="grid-cols-[200px_minmax(900px,_1fr)_100px]"></div>

<!-- v3: var() trong arbitrary -->
<div class="w-[var(--sidebar-width)]"></div>

v4 giữ nguyên cú pháp này nhưng xử lý calc()var() mượt hơn - không cần escape phức tạp. Quan trọng hơn, v4 thêm wildcard * support cho custom utilities:

@utility sidebar-w-* {
  width: --value([*]);
}

Sau đó dùng: class="sidebar-w-[250px]" hoặc class="sidebar-w-[--custom-var]". Pattern này cho phép build utility system linh hoạt như Chakra nhưng không cần JS runtime.

343581 Wildcard utility - viết một lần, dùng với bất kỳ giá trị nào

Arbitrary variants cũng được cải thiện. Bạn có thể combine container queries với arbitrary:

<!-- Chỉ áp dụng khi container đạt @sm VÀ nằm trong .dashboard -->
<div class="[.dashboard_&@sm]:grid-cols-2"></div>

Một tip thực tế: arbitrary values vẫn cần dùng cho các trường hợp design cụ thể như pixel-perfect layout, custom breakpoint lẻ, hoặc giá trị từ design token của Figma. Nhưng cho spacing/sizing thông thường, default scale của v4 đủ cover {{fact:fact_1}} trường hợp phổ biến mà không cần đụng tới [...].


OKLCH color space: màu sắc đều hơn, đẹp hơn, dễ quản lý hơn

Bạn có bao giờ nhận thấy trong design system, màu blue-500 trông sáng hơn green-500 dù cùng "level 500"? Đó là vấn đề của HSL và HEX - chúng không phản ánh đúng độ sáng mà mắt người cảm nhận.

OKLCH giải quyết đúng vấn đề đó. Thay vì Hue/Saturation/Lightness theo kiểu máy tính, OKLCH dùng Lightness theo nhận thức thị giác (perceptual lightness). Kết quả: oklch(60% 0.15 260) (xanh dương) và oklch(60% 0.15 140) (xanh lá) sẽ có độ sáng thật sự bằng nhau khi mắt nhìn vào.

343582 Cùng lightness value trong OKLCH = cùng độ sáng thật sự - không giống HEX/HSL

Trong Tailwind v4, dùng OKLCH qua CSS variables trong @theme:

@theme {
  /* Màu chính - dễ tạo palette đồng nhất */
  --color-brand-50:  oklch(97% 0.02 260);
  --color-brand-100: oklch(93% 0.05 260);
  --color-brand-500: oklch(60% 0.15 260);
  --color-brand-900: oklch(25% 0.08 260);

  /* Chỉ đổi hue (số cuối) để tạo màu mới */
  --color-success: oklch(60% 0.15 140); /* xanh lá */
  --color-warning: oklch(60% 0.15 85);  /* vàng cam */
  --color-danger:  oklch(60% 0.15 20);  /* đỏ */
}

Nhìn vào pattern: success, warning, danger đều dùng cùng lightness (60%) và chroma (0.15), chỉ khác hue. Điều đó đảm bảo 3 màu trông "nặng" bằng nhau trên giao diện - rất quan trọng cho badge, alert, button states.

Dark mode với OKLCH cũng cực sạch:

@theme {
  --color-bg: oklch(98% 0.01 240);   /* gần trắng */
  --color-text: oklch(15% 0.02 240); /* gần đen */
}

@media (prefers-color-scheme: dark) {
  @theme {
    --color-bg: oklch(12% 0.02 240);  /* gần đen */
    --color-text: oklch(92% 0.01 240); /* gần trắng */
  }
}

Browser support: OKLCH được hỗ trợ từ {{fact:fact_2}}. Với các project cần IE hay browser cũ hơn, fallback về HEX vẫn ổn.


Container queries native: responsive theo container, không chỉ theo viewport

Media queries có một điểm yếu kinh điển: chúng responsive theo viewport (cửa sổ trình duyệt), không phải theo container (element cha). Kết quả là cùng một component card, khi nằm trong sidebar hẹp vs nội dung chính rộng, nó không thể tự biết mình đang ở đâu để điều chỉnh.

Tailwind v3 giải quyết bằng plugin @tailwindcss/container-queries. v4 tích hợp thẳng vào core - không cần cài thêm gì.

<!-- Đánh dấu container -->
<div class="@container">
  <!-- Card tự responsive theo độ rộng của container cha -->
  <div class="grid grid-cols-1 @sm:grid-cols-2 @lg:grid-cols-3 gap-6">
    <div class="@container p-4 @md:p-6 @md:flex @md:gap-4">
      <img class="w-full @md:w-32 @md:h-32 rounded-lg" />
      <div class="@md:flex-1">
        <h3 class="text-sm @md:text-base font-semibold">Tiêu đề</h3>
        <p class="text-xs @md:text-sm text-gray-500">Mô tả</p>
      </div>
    </div>
  </div>
</div>

343583 Cùng component, tự reshape theo container - không phụ thuộc viewport

Breakpoints container mặc định trong v4: @xs (320px), @sm (384px), @md (448px), @lg (512px), @xl (576px), @2xl (672px). Custom breakpoint qua @theme:

@theme {
  --container-sidebar: 280px;
  --container-chart: 480px;
}

Sau đó dùng: class="@sidebar:hidden @chart:block".

Tại sao quan trọng cho dashboard? Một widget chart khi nằm trong layout 3 cột sẽ compact hơn so với khi mở rộng sang full-width. Container queries cho phép widget tự điều chỉnh mà không cần JavaScript ResizeObserver hay CSS hack. Bạn viết component một lần, nó hoạt động đúng ở mọi vị trí.

Kết hợp với arbitrary variants: class="[@container(min-width:400px)_&]:flex-row" cho trường hợp cần precision cao hơn preset breakpoints.


Build responsive dashboard thực chiến với Tailwind v4

Đủ lý thuyết rồi - build thật đi. Mình sẽ dùng tất cả những gì đã nói để tạo một dashboard layout responsive hoàn chỉnh.

Bước 1: Setup CSS-first config

/* styles/app.css */
@import "tailwindcss";

@theme {
  /* Typography */
  --font-sans: 'Be Vietnam Pro', system-ui, sans-serif;

  /* Brand colors - OKLCH palette đồng nhất */
  --color-brand-50:  oklch(97% 0.02 250);
  --color-brand-500: oklch(55% 0.18 250);
  --color-brand-900: oklch(22% 0.08 250);

  /* Status colors - cùng lightness, khác hue */
  --color-success: oklch(55% 0.18 145);
  --color-warning: oklch(55% 0.18 80);
  --color-danger:  oklch(55% 0.18 20);

  /* Layout tokens */
  --sidebar-width: 260px;
  --header-height: 64px;
}

Bước 2: Layout shell

<!-- Dashboard grid chính -->
<div class="min-h-screen bg-[--color-brand-50]">
  <!-- Header -->
  <header class="h-[--header-height] bg-white border-b px-6 flex items-center">
    <nav class="flex items-center gap-4">
      <span class="font-semibold text-[--color-brand-500]">Dashboard VN</span>
    </nav>
  </header>

  <div class="flex">
    <!-- Sidebar - ẩn trên mobile -->
    <aside class="hidden md:block w-[--sidebar-width] min-h-screen bg-[--color-brand-900] p-6">
      <!-- Nav items -->
    </aside>

    <!-- Main content - @container để widget tự responsive -->
    <main class="flex-1 @container p-6 md:p-9">
      <!-- Widget grid -->
      <div class="grid grid-cols-1 @md:grid-cols-2 @xl:grid-cols-4 gap-7">
        <!-- KPI Card -->
        <div class="bg-white rounded-2xl p-6 @container">
          <p class="text-sm text-gray-500">Doanh thu</p>
          <p class="text-2xl @lg:text-3xl font-bold text-[--color-brand-900]">2.4 tỷ</p>
          <span class="text-xs bg-[--color-success] text-white px-2 py-0.5 rounded-full">
            +12.5%
          </span>
        </div>
      </div>

      <!-- Chart section - responsive theo container -->
      <div class="mt-7 @container">
        <div class="grid grid-cols-1 @lg:grid-cols-[2fr_1fr] gap-7">
          <div class="bg-white rounded-2xl p-6"><!-- Line chart --></div>
          <div class="bg-white rounded-2xl p-6"><!-- Donut chart --></div>
        </div>
      </div>
    </main>
  </div>
</div>

343584 Dashboard responsive - widget tự điều chỉnh không cần JS

Mấy điểm đáng chú ý trong code trên:

  • gap-7p-9 dùng được thẳng mà không cần [28px] hay [36px]
  • w-[--sidebar-width] lấy giá trị từ CSS var định nghĩa trong @theme
  • @md:grid-cols-2@xl:grid-cols-4 là container breakpoints, không phải viewport
  • bg-[--color-brand-900]text-[--color-brand-500] dùng OKLCH colors đã định nghĩa

Bạn có thể tải file demo để xem toàn bộ layout chạy thật: {{resource:resource_0}}


Migration từ v3 lên v4: những gì cần làm

Nếu bạn đang có project v3, migration lên v4 không phải đơn giản install rồi chạy - có một số breaking changes cần xử lý.

Những gì thay đổi:

tailwind.config.js không còn là cách chính để config. Bạn cần chuyển theme.extend vào @theme trong CSS:

// v3 - tailwind.config.js (cũ)
module.exports = {
  theme: {
    extend: {
      colors: { brand: '#3b82f6' },
      spacing: { 18: '4.5rem' }
    }
  }
}
/* v4 - app.css (mới) */
@theme {
  --color-brand: oklch(60% 0.18 250); /* đổi luôn sang OKLCH */
  --spacing-18: 4.5rem;
}

Plugins cũng thay đổi. Thay vì plugins: [require('...')] trong JS, dùng @plugin directive:

@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";

Nếu trước đây dùng resolveConfig để access theme trong JavaScript:

// v3 (cũ)
import resolveConfig from 'tailwindcss/resolveConfig'
import config from './tailwind.config.js'
const theme = resolveConfig(config)
const primary = theme.theme.colors.brand

// v4 (mới)
const primary = getComputedStyle(document.documentElement)
  .getPropertyValue('--color-brand')

343585 4 bước migration từ v3 sang v4 - chú ý phần custom utilities

Quy trình migration an toàn:

  1. Chạy npx tailwindcss@next init --css để tạo boilerplate CSS config mới
  2. Copy từng phần theme.extend từ JS sang @theme trong CSS
  3. Đổi màu HEX/HSL sang OKLCH (dùng công cụ convert online)
  4. Rewrite custom plugins thành @utility directives
  5. Test kỹ purging - v4 có cơ chế detect class khác v3

Lưu ý cho dự án Việt: Nếu dùng font tiếng Việt (Be Vietnam Pro, SVN-Gilroy), khai báo thẳng trong @theme:

@theme {
  --font-sans: 'Be Vietnam Pro', system-ui, sans-serif;
}

Nếu bạn mới bắt đầu với HTML/CSS và chưa quen với Tailwind, khóa HTML CSS từ Zero đến Hero trên F8 sẽ cho bạn nền tảng tốt trước khi đụng vào v4. Còn nếu muốn học responsive đúng bài bản, khóa Responsive Với Grid System cũng rất đáng xem.

Một điều mình thấy sau khi migrate 2 project: code CSS sạch hơn hẳn. Không còn file JS config lủng củng, không còn phải jump qua lại giữa CSS và config. Mọi thứ nằm trong CSS - đúng chỗ của nó.