Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 23 — CSS Variables (Custom Properties)

Web Design Basics Bài 23/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn vừa nhận một dự án website cho một chuỗi cà phê đang mở rộng ở Sài Gòn. Bạn dùng màu cam đặc trưng #F97316 cho nút bấm, cho tiêu đề, cho icon, cho viền card — tổng cộng 140 chỗ trong file CSS. Ba tuần sau, giám đốc marketing nhắn: "Em ơi, brand vừa đổi sang cam đậm hơn một chút, đổi giúp anh nhé." Nếu bạn từng phải mở file CSS dài 2000 dòng và bấm Ctrl+H để tìm-thay-thế thủ công, bạn sẽ hiểu ngay nỗi đau này: bạn không chắc đã đổi hết chưa, có chỗ nào dùng #f97316 viết thường, có chỗ nào dùng rgb(249, 115, 22), và lỡ tay đổi nhầm một màu cam khác không liên quan.

CSS Variables (tên chính thức là CSS Custom Properties) sinh ra để giải quyết chính xác vấn đề này. Thay vì viết giá trị màu, khoảng cách, bo góc rải rác khắp nơi, bạn khai báo chúng một lần ở một nơi tập trung, đặt tên có ý nghĩa, rồi tham chiếu lại ở mọi chỗ cần dùng. Đổi một dòng, toàn bộ website cập nhật theo.

Đây không phải tính năng "có cũng được". Nó là nền móng cho mọi thứ hiện đại trong thiết kế web: theming (sáng/tối), design system, và đặc biệt là design tokens mà bạn sẽ học ở Bài 39. Hiểu chắc CSS Variables hôm nay là bạn đã đặt một viên gạch quan trọng cho cả phần còn lại của khóa học. Hãy bắt đầu.

Khái niệm cốt lõi

CSS Variable là gì

CSS Variable là một giá trị bạn tự đặt tên và lưu lại để dùng nhiều lần. Tên của nó luôn bắt đầu bằng hai dấu gạch ngang --, và bạn lấy giá trị ra bằng hàm var().

:root {
  --color-primary: #F97316;
}

.button { background-color: var(--color-primary); }

Ở đây --color-primary là biến, còn var(--color-primary) là cách "đọc" giá trị của biến đó ra. Trình duyệt sẽ thay var(--color-primary) bằng #F97316 khi render.

:root và phạm vi (scope)

Bạn thấy :root ở ví dụ trên chứ? Đây là một selector trỏ tới phần tử gốc của trang (tương đương thẻ <html>, nhưng có độ ưu tiên cao hơn một chút). Khai báo biến trong :root nghĩa là biến đó có hiệu lực toàn cục — mọi phần tử trong trang đều dùng được.

Nhưng điểm tinh tế và mạnh mẽ nhất của CSS Variables là chúng tuân theo cơ chế kế thừa (inheritance) và có thể được khai báo ở phạm vi hẹp hơn:

:root {
  --color-text: #1F2937; / xám đậm, dùng toàn trang /
}

.card-dark { --color-text: #F9FAFB; / chỉ trong .card-dark, chữ chuyển sang trắng / }

.card-dark p { color: var(--color-text); / lấy giá trị gần nhất: #F9FAFB / }

Phần tử con sẽ dùng giá trị biến của tổ tiên gần nhất đã định nghĩa nó. Đây chính là chìa khóa để làm dark mode và component có nhiều biến thể mà không cần viết lại hàng loạt rule.

Giá trị dự phòng (fallback)

var() cho phép bạn truyền giá trị thứ hai làm "phương án dự phòng" nếu biến chưa được định nghĩa:

.button {
  background-color: var(--color-brand, #3B82F6);
}

Nếu --color-brand không tồn tại, trình duyệt dùng #3B82F6. Rất hữu ích khi viết component để chia sẻ, hoặc khi bạn chưa chắc biến đã được khai báo ở mọi ngữ cảnh.

Khác biệt với biến của Sass/Less

Nhiều bạn từng dùng Sass với $color-primary sẽ thắc mắc: khác gì nhau? Khác biệt cốt tử:

  • Biến Sass ($) được xử lý lúc biên dịch (compile time). Khi file CSS được tạo ra, giá trị đã bị "đóng băng" — trình duyệt không hề biết tới biến đó.
  • CSS Variable (--) sống lúc chạy (runtime) ngay trong trình duyệt. Bạn có thể đọc, thay đổi nó bằng JavaScript, và nó phản ứng theo cascade, theo media query, theo trạng thái :hover. Đây là điều Sass không bao giờ làm được.
Nói ngắn gọn: biến Sass là "tìm và thay thế thông minh", còn CSS Variable là một giá trị động, sống trong trình duyệt.

Một số quy tắc cần nhớ

  • Tên biến phân biệt hoa thường: --myColor--mycolor là hai biến khác nhau.
  • Giá trị có thể là bất cứ thứ gì: màu, số có đơn vị, chuỗi, thậm chí cả một đoạn giá trị phức hợp như 1px solid #ccc.
  • Biến không dùng được cho tên thuộc tính hay một phần selector — chỉ dùng làm giá trị. Ví dụ var(--prop): red; là sai.

Tình huống thực tế

Ví dụ 1 — Chuỗi cà phê đổi màu thương hiệu trong 30 giây

Quay lại câu chuyện mở đầu. Giả sử bạn làm cho The Coffee House (hoặc một chuỗi giả định tương tự) và đã lỡ hardcode màu cam khắp 140 chỗ. Bài học đau thương đó khiến bạn refactor lại:

:root {
  --brand-orange: #F97316;
}

.btn-primary { background: var(--brand-orange); } .heading { color: var(--brand-orange); } .icon-cart { fill: var(--brand-orange); } .card { border-top: 3px solid var(--brand-orange); } / ...và 136 chỗ khác đều dùng var(--brand-orange) /

Lần sau khi marketing yêu cầu đổi sang cam đậm #EA580C, bạn chỉ sửa một dòng trong :root. Toàn bộ 140 điểm chạm cập nhật đồng loạt, không sót chỗ nào, không lo viết hoa viết thường lẫn lộn.

Bài học rút ra: Mỗi giá trị thiết kế lặp lại từ 3 lần trở lên là ứng cử viên sáng giá để tách thành biến. Bạn không chỉ tiết kiệm thời gian sửa, mà còn loại bỏ hoàn toàn nguy cơ "đổi thiếu một chỗ".

Ví dụ 2 — Tiki làm Dark Mode mà không nhân đôi CSS

Một sàn thương mại điện tử như Tiki muốn thêm chế độ tối. Cách làm thủ công kiểu cũ là viết một bộ CSS cho sáng, rồi copy toàn bộ và sửa màu cho tối — file phình gấp đôi, bảo trì ác mộng. Với CSS Variables, bạn chỉ cần đảo các biến:

:root {
  --bg-page:   #FFFFFF;
  --bg-card:   #F3F4F6;
  --text-main: #111827;
}

[data-theme="dark"] { --bg-page: #0F172A; --bg-card: #1E293B; --text-main: #E2E8F0; }

.page { background: var(--bg-page); } .card { background: var(--bg-card); color: var(--text-main); }

Toàn bộ component (.page, .card...) không cần biết gì về theme. Chúng chỉ tham chiếu biến. Khi JavaScript gắn data-theme="dark" vào thẻ <html>, mọi biến bị ghi đè và giao diện chuyển tối ngay lập tức:

document.documentElement.setAttribute('data-theme', 'dark');

Bài học rút ra: Khi component chỉ phụ thuộc vào tên biến ngữ nghĩa (--bg-card) chứ không phụ thuộc giá trị màu cụ thể, bạn có thể đổi toàn bộ "bộ da" của sản phẩm chỉ bằng cách đổi giá trị biến ở một nơi. Đây là tư duy nền tảng của design tokens.

Ví dụ 3 — Startup fintech ở Singapore và hệ thống spacing nhất quán

Một startup fintech giả định tên PayLah Pro có 4 dev frontend, mỗi người tự "cảm tính" về khoảng cách: người dùng padding: 14px, người 16px, người 15px. Kết quả là giao diện trông lệch lạc, thiếu chuyên nghiệp. Họ thống nhất một thang khoảng cách bằng biến:

:root {
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 40px;
  --radius-md: 8px;
}

.form-input { padding: var(--space-sm) var(--space-md); border-radius: var(--radius-md); margin-bottom: var(--space-lg); }

Giờ cả team chỉ được phép dùng các biến này. Không còn 13px hay 17px lung tung. Code review cũng dễ hơn: thấy số "lẻ" hardcode là biết ngay cần thay bằng biến.

Bài học rút ra: CSS Variables không chỉ để chống lặp, mà còn là công cụ kỷ luật thiết kế. Khi cả nhóm dùng chung một bộ biến giới hạn, sản phẩm tự khắc trở nên nhất quán mà không cần ai đó kiểm tra từng dòng.

Hướng dẫn từng bước

Giả sử bạn đang xây một component nút bấm có nhiều trạng thái cho một dự án thật. Làm theo trình tự sau:

Bước 1 — Liệt kê các giá trị lặp lại. Mở thiết kế, gạch chân mọi màu, khoảng cách, bo góc, cỡ chữ xuất hiện nhiều lần. Đây sẽ là danh sách biến của bạn.

Bước 2 — Khai báo biến trong :root. Đặt tên theo ý nghĩa, không theo giá trị. Viết --color-primary chứ đừng viết --orange. Lý do: nếu mai mốt brand đổi sang xanh, cái tên --orange sẽ thành sai nghĩa, còn --color-primary vẫn đúng.

:root {
  --color-primary: #F97316;
  --color-primary-hover: #EA580C;
  --color-text-on-primary: #FFFFFF;
  --space-md: 16px;
  --radius-md: 8px;
}

Bước 3 — Tham chiếu biến bằng var(). Thay mọi giá trị hardcode trong component:

.button {
  background-color: var(--color-primary);
  color: var(--color-text-on-primary);
  padding: var(--space-md);
  border-radius: var(--radius-md);
  border: none;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.button:hover { background-color: var(--color-primary-hover); }

Bước 4 — Tạo biến thể bằng scope hẹp. Muốn có nút "nguy hiểm" (xóa) màu đỏ? Đừng viết lại cả khối, chỉ ghi đè biến trong phạm vi class đó:

.button--danger {
  --color-primary: #DC2626;
  --color-primary-hover: #B91C1C;
}

.button đã dùng var(--color-primary), chỉ cần ghi đè biến là toàn bộ nút đổi màu, kể cả trạng thái hover.

Bước 5 — Kiểm tra fallback (nếu cần). Nếu component có thể được dùng ở nơi chưa khai báo biến, thêm giá trị dự phòng: var(--color-primary, #3B82F6).

Bước 6 — Kiểm tra trong DevTools. Mở trình duyệt, bấm chuột phải > Inspect, chọn phần tử. Trong tab Styles bạn sẽ thấy biến và giá trị tính toán (computed value). Đây là cách nhanh nhất để debug khi màu "không lên đúng".

Lỗi thường gặp & mẹo

Lỗi 1 — Quên hai dấu gạch ngang. Viết var(-color-primary) (một gạch) hoặc khai báo color-primary: red (không gạch). CSS sẽ im lặng bỏ qua, không báo lỗi, và bạn ngồi gãi đầu không hiểu sao màu không lên. Luôn nhớ: hai gạch cả khi khai báo lẫn khi gọi.

Lỗi 2 — Đặt tên theo giá trị thay vì ý nghĩa. --blue: #3B82F6 nghe có vẻ ổn cho tới ngày bạn phải đổi nó thành màu xanh lá vì lý do thương hiệu — lúc đó background: var(--blue) cho ra màu xanh lá trông cực kỳ vô lý khi đọc code. Hãy đặt tên theo vai trò: --color-primary, --color-success, --color-danger.

Lỗi 3 — Lạm dụng, biến hóa mọi thứ. Không phải giá trị nào cũng cần thành biến. Một margin chỉ dùng đúng một lần ở một chỗ thì cứ viết thẳng số. Tách biến cho thứ chỉ xuất hiện một lần chỉ làm code khó đọc thêm. Nguyên tắc: lặp từ 3 lần, hoặc là giá trị thuộc "hệ thống thiết kế" (màu brand, thang spacing) thì mới tách.

Lỗi 4 — Tưởng var() dùng được trong media query. Bạn không thể viết @media (min-width: var(--bp-md)). CSS Variables không hoạt động bên trong điều kiện của media query. Đây là giới hạn bạn cần biết để khỏi mất thời gian.

Mẹo 1 — Tính toán với calc(). Biến kết hợp calc() rất mạnh:

:root { --space-md: 16px; }
.section { padding: calc(var(--space-md)  2); } / = 32px */

Mẹo 2 — Đọc/ghi biến bằng JavaScript. Đây là sức mạnh runtime mà Sass không có:

// Đọc
const c = getComputedStyle(document.documentElement)
            .getPropertyValue('--color-primary');
// Ghi
document.documentElement.style.setProperty('--color-primary', '#10B981');

Cực hữu ích cho tính năng cho người dùng tự chọn màu chủ đạo (theme picker).

Mẹo 3 — Gom biến vào một file riêng. Trong dự án thật, hãy để toàn bộ khai báo :root trong một file như tokens.css hoặc variables.css và import đầu tiên. Cả team biết chính xác nơi tìm và sửa.

Bài tập thực hành

  • Refactor cơ bản. Lấy một đoạn CSS bất kỳ bạn từng viết (hoặc tạo mới một card sản phẩm). Tìm tất cả màu và khoảng cách hardcode, tách chúng thành biến trong :root với tên ngữ nghĩa. Đảm bảo giao diện trông y hệt sau khi refactor.
  • Làm Dark Mode. Tạo một trang đơn giản gồm tiêu đề, một đoạn văn và một card. Khai báo biến màu nền/chữ trong :root, rồi tạo khối [data-theme="dark"] ghi đè các biến đó. Viết vài dòng JavaScript gắn một nút bấm để chuyển qua lại giữa hai chế độ.
  • Component có biến thể. Tạo một nút .button dùng biến --color-primary. Sau đó tạo ba class .button--success, .button--danger, .button--ghost chỉ bằng cách ghi đè biến trong scope của từng class — không được viết lại thuộc tính background ở mỗi biến thể.
  • Thang spacing. Định nghĩa một bộ --space-xs đến --space-xl, rồi dựng một form đăng nhập (label, input, nút) chỉ dùng các biến spacing này cho mọi padding/margin. Mục tiêu: không có một con số px nào "lẻ" nằm rải rác trong code component.
  • Thử thách JavaScript. Thêm vào bài tập 2 một ô chọn màu (<input type="color">). Khi người dùng đổi màu, dùng setProperty để cập nhật --color-primary và quan sát toàn trang đổi màu theo thời gian thực.

Tóm tắt

  • CSS Variables (Custom Properties) cho phép khai báo giá trị một lần với tên bắt đầu bằng --, rồi tái sử dụng bằng hàm var().
  • Khai báo trong :root để dùng toàn cục; khai báo trong selector hẹp hơn để ghi đè theo phạm vi — đây là nền tảng của theming và biến thể component.
  • Khác với biến Sass (compile-time, tĩnh), CSS Variables sống runtime: phản ứng theo cascade, đọc/ghi được bằng JavaScript, làm dark mode và theme picker dễ dàng.
  • Đặt tên theo ý nghĩa/vai trò (--color-primary), không theo giá trị (--orange). Dùng fallback var(--x, mặc-định) khi cần an toàn.
  • Tách biến cho giá trị lặp lại hoặc thuộc hệ thống thiết kế; đừng lạm dụng cho thứ chỉ dùng một lần. Nhớ: var() không dùng được trong điều kiện media query.
Nắm vững CSS Variables, bạn đã sẵn sàng cho những bài nâng cao về design tokens và Tailwind sắp tới. Đây là một trong những kỹ năng cho "lãi kép" cao nhất trong cả khóa — hãy luyện cho thật nhuần.