Bỏ qua, sang nội dung chính
← Về danh sách

· Working ·

Dark mode trong design system: câu hỏi thường gặp về token và tương phản WCAG

Dark mode có cần thiết kế riêng không, tỉ lệ tương phản bao nhiêu là đạt WCAG AA, vì sao màu xám nhạt và màu thương hiệu hay trượt chuẩn. Trả lời bằng số đo thật và CSS thật của site này.

Dark mode cần một lượt kiểm riêng, nhưng không cần vẽ lại giao diện. Nếu component chỉ gọi token ngữ nghĩa như background, foreground, muted, bạn chỉ phải đổi giá trị ở lớp token rồi đo lại tương phản. Mốc cần đạt theo WCAG 2.2 AA là 4.5:1 cho chữ thường và 3:1 cho chữ lớn.

Tôi là Hai Le, UI/UX designer biết code. Mọi số đo trong bài lấy từ màu thật của site bạn đang đọc, tôi tính lại bằng công thức của WCAG.

Dark mode có cần thiết kế riêng không?

Cần một lượt thiết kế cho phần màu, còn bố cục và component giữ nguyên.

Phần việc gồm ba bước: chọn giá trị tối cho thang xám, chọn bậc màu thương hiệu đọc được trên nền tối, và đo lại mọi cặp chữ trên nền. Nếu component đang ghi cứng mã màu, bạn còn phải sửa từng component.

Ở site này, thang xám được đánh số theo kiểu Radix: số bậc là khoảng cách so với nền, nên neutral-50 luôn là nền và neutral-950 luôn là chữ đậm nhất ở cả hai chế độ. Bài Thang màu kiểu Radix giải thích chi tiết cách đánh số này.

Tỉ lệ tương phản bao nhiêu là đạt WCAG AA?

Bảng dưới tóm tắt hai tiêu chí liên quan nhất trong WCAG 2.2:

Thứ cần đoTiêu chíNgưỡng AA
Chữ thường1.4.34.5:1
Chữ lớn: từ 18pt (khoảng 24px), hoặc 14pt đậm (khoảng 18.5px)1.4.33:1
Viền, icon, trạng thái của thành phần giao diện1.4.113:1 với màu kề bên
Thành phần bị vô hiệu, chữ trang trí, chữ trong logo1.4.3, 1.4.11Không bắt buộc

Tài liệu giải thích của W3C còn ghi một chi tiết hay bị bỏ qua: tỉ lệ không được làm tròn. Một cặp màu đo được 4.499:1 là trượt ngưỡng 4.5:1.

Đây là các cặp màu thật của site, đo ở cả hai chế độ:

Cặp chữ trên nềnSángTối
foreground trên background21.00:116.13:1
muted-foreground trên background7.00:17.94:1
subtle-foreground trên background4.88:15.19:1
subtle-foreground trên muted4.60:14.83:1
primary-text trên background6.10:15.94:1

Cặp yếu nhất là chữ xám bậc ba trên mảng xám, 4.60:1. Tôi kiểm cặp này đầu tiên mỗi khi đổi một giá trị xám.

Làm dark mode bằng token thay vì đảo màu thủ công thế nào?

Đảo màu thủ công nghĩa là viết text-gray-900 dark:text-white ở mọi component. Tài liệu Tailwind dùng đúng cách này trong ví dụ của nó, và với một trang nhỏ thì không sao. Khi hệ thống lớn lên, mỗi class dark: là một chỗ có thể quên.

Site này làm ngược lại: theme là một class dark trên thẻ html, và mọi thay đổi màu nằm trong app/globals.css. Biến thể dark của Tailwind được khai bằng một dòng:

@custom-variant dark (&:is(.dark *));

Tài liệu Tailwind gợi ý dạng &:where(.dark, .dark *), khớp cả chính phần tử mang class. Site này chỉ đặt class trên html, nên dạng :is(.dark *) đủ dùng.

Một script nhỏ chạy trong head trước khi trang vẽ, đọc lựa chọn đã lưu hoặc prefers-color-scheme của hệ điều hành, rồi bật class. Bản rút gọn từ lib/theme-init-script.ts:

var e = localStorage.getItem('theme');
var d = e === 'dark' ||
  ((!e || e === 'system') &&
   window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', d);

Phần còn lại là token. Thang xám đổi giá trị dưới .dark, còn lớp ngữ nghĩa chỉ phải đổi bậc cho những màu không tự đảo:

.dark {
  --primary: var(--primary-400);
  --destructive: var(--error-400);
  --primary-foreground: #ffffff;
  --primary-text: var(--primary-400);
}

Nếu bạn mới làm quen ba lớp token, đọc bài Design token là gì trước.

Vì sao chữ xám nhạt hay trượt chuẩn tương phản?

Màu xám nhạt trông vẫn đọc được trên một màn hình tốt, nhưng đo ra thì thiếu. Hai lỗi tôi gặp nhiều nhất:

Dùng bậc trang trí làm chữ. #9a9a9a chỉ đạt 2.81:1 trên nền trắng và 2.65:1 trên mảng #f8f8f8. Site này cấm hẳn neutral-500 và neutral-400 làm màu chữ.

Ghi cứng giá trị sáng. Một caption ghi cứng #717171 đạt 4.60:1 trên mảng xám sáng. Sang dark mode, mảng đó thành #191919 và cùng caption chỉ còn 3.60:1. Nếu caption gọi text-subtle-foreground, nó tự đổi sang #868686 và đạt 4.83:1.

Cách chặn cả hai là giới hạn số bậc chữ. Site này có đúng bốn bậc xám cho chữ, đặt theo ngưỡng tương phản trước khi chỉnh theo mắt. Muốn thêm bậc thứ năm thì gần như chắc chắn bố cục đang có vấn đề.

Chữ màu thương hiệu có đạt tương phản không?

Tuỳ màu, và phải đo ở cả hai chế độ. Magenta #ff009d của site này đạt 3.66:1 trên nền trắng. Con số đó đủ cho chữ lớn và cho viền thành phần giao diện, nhưng trượt với chữ thường.

Nên site tách hai token. primary là màu thương hiệu, dùng cho mảng nền. primary-text là bậc đậm hơn #be0074, đạt 6.10:1, dùng cho link và nhãn magenta từ 20px trở xuống.

Dark mode đảo bài toán. #be0074 trên nền #111111 chỉ còn 3.09:1. Vì vậy dưới .dark, primary-text chuyển sang #ff3fb2, đạt 5.94:1. Nếu bạn chỉ có một token màu thương hiệu cho cả chữ lẫn nền, dark mode gần như chắc chắn làm vỡ một trong hai.

Nhãn chữ trắng trên nút #ff009d cũng chỉ đạt 3.66:1. Theo tiêu chí 1.4.3, nhãn nút vẫn là chữ, nên cặp này chỉ qua ngưỡng khi chữ đủ lớn. Tôi ghi nó ở đây như một điểm cần xử lý tiếp của chính site này.

Nền dark mode nên là đen tuyền hay xám rất tối?

Site này dùng #111111 cho nền trang ở chế độ tối và #000000 cho footer. Footer là dải đậm nhất trang ở chế độ sáng, và tôi muốn giữ vai trò đó khi chuyển theme. Nếu nền trang đã là đen tuyền, footer không còn chỗ để đậm hơn.

Các mảng nền cũng cần cách nhau đủ để nhìn ra: nền #111111, mảng #191919, đường kẻ #242424. Khi đặt nền tối, hãy kiểm cả bậc mảng và bậc viền, chứ đừng chỉ nhìn cặp chữ trên nền.

Kiểm tra dark mode ở đâu trước khi ship?

Tôi làm theo thứ tự này:

  1. Bật chế độ tối của hệ điều hành, tải lại trang, xem có nháy sáng trước khi vào theme tối không.
  2. Dùng nút đổi theme để chuyển qua lại vài lần trên cùng một trang.
  3. Đo các cặp có tỉ lệ thấp nhất trong bảng ở trên, ở cả hai chế độ.
  4. Tìm mã màu ghi cứng trong component bằng một lệnh tìm chuỗi # trong thư mục component.

Bước 4 là việc hợp để nhờ một AI agent như Claude Code chạy và giải thích kết quả. Cách tôi làm việc cùng nó nằm ở bài Claude Code cho designer. Các câu hỏi về đặt tên token có ở bài Câu hỏi thường gặp về token.

Tôi từng dựng hệ màu với thang tím, thang cam và thang xám cho Joyme, chi tiết ở case study Joyme. Nếu team bạn cần người dựng design system có dark mode đi tới tận code, xem thêm về tôi ở trang giới thiệu.

Nguồn

Câu hỏi thường gặp

Dark mode có cần thiết kế riêng không?
Cần một lượt kiểm riêng, nhưng không cần vẽ lại từng màn hình. Nếu component chỉ dùng token ngữ nghĩa, phần việc chính là chọn giá trị tối cho thang xám và chọn bậc khác cho màu thương hiệu, rồi đo lại tương phản.
Tỉ lệ tương phản bao nhiêu là đạt WCAG AA?
Theo WCAG 2.2, chữ thường cần ít nhất 4.5:1, chữ lớn (từ 24px, hoặc khoảng 18.5px đậm) cần 3:1. Thành phần giao diện và đồ hoạ cần phân biệt được cần 3:1 với màu kề bên. Tỉ lệ không được làm tròn lên.
Màu xám nào được dùng làm chữ?
Ở site này chỉ có bốn bậc: neutral-950, 900, 700 và 600. Bậc 600 (#717171) đạt 4.88:1 trên nền trắng và 4.60:1 trên mảng xám. Bậc 500 (#9a9a9a) chỉ đạt 2.81:1 nên dành cho trang trí và trạng thái vô hiệu.
Nền dark mode có nên là đen tuyền #000000 không?
Site này dùng #111111 cho nền trang ở chế độ tối và giữ #000000 cho footer, để footer vẫn là dải đậm nhất trang. Chọn giá trị nào cũng được, miễn cặp chữ trên nền vẫn đạt ngưỡng và các bề mặt còn phân biệt được với nhau.