· 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 đo | Tiêu chí | Ngưỡng AA |
|---|---|---|
| Chữ thường | 1.4.3 | 4.5:1 |
| Chữ lớn: từ 18pt (khoảng 24px), hoặc 14pt đậm (khoảng 18.5px) | 1.4.3 | 3:1 |
| Viền, icon, trạng thái của thành phần giao diện | 1.4.11 | 3:1 với màu kề bên |
| Thành phần bị vô hiệu, chữ trang trí, chữ trong logo | 1.4.3, 1.4.11 | Khô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ền | Sáng | Tối |
|---|---|---|
foreground trên background | 21.00:1 | 16.13:1 |
muted-foreground trên background | 7.00:1 | 7.94:1 |
subtle-foreground trên background | 4.88:1 | 5.19:1 |
subtle-foreground trên muted | 4.60:1 | 4.83:1 |
primary-text trên background | 6.10:1 | 5.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:
- 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.
- Dùng nút đổi theme để chuyển qua lại vài lần trên cùng một trang.
- Đo các cặp có tỉ lệ thấp nhất trong bảng ở trên, ở cả hai chế độ.
- 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
- W3C: Understanding SC 1.4.3 Contrast (Minimum), WCAG 2.2: ngưỡng 4.5:1 và 3:1, định nghĩa chữ lớn, không làm tròn. Truy cập 2026-10-05.
- W3C: Understanding SC 1.4.11 Non-text Contrast, WCAG 2.2: ngưỡng 3:1 cho thành phần giao diện, ngoại lệ cho thành phần bị vô hiệu. Truy cập 2026-10-05.
- Tailwind CSS: Dark mode: biến thể
dark,@custom-variant, script tránh nháy. Truy cập 2026-10-05. - Radix Colors: Usage. Truy cập 2026-10-05.
app/globals.css,lib/theme-init-script.tsvàDESIGN.mdtrong repo của site này.
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.
Có thể bạn quan tâm
Claude Code là gì? Cài đặt và chạy lệnh đầu tiên khi chưa biết code
Hướng dẫn cho người chưa từng mở terminal: Claude Code là gì, cần chuẩn bị gì, cài trên macOS và Windows, chạy phiên đầu tiên và sửa các lỗi hay gặp. Mọi lệnh lấy từ tài liệu chính thức của Anthropic.
Working· 2026-10-04
Claude Code cho designer: biến thiết kế Figma thành trang chạy được
Cách một designer dùng Claude Code để đưa thiết kế từ Figma thành trang web thật: chuẩn bị gì, viết quy tắc cho AI ra sao, quy trình sáu bước và những lỗi tôi đã gặp khi dựng chính site này.
Working· 2026-10-04
Claude là gì? Hướng dẫn cho người chưa dùng AI bao giờ
Claude là trợ lý AI của Anthropic. Bài này giải thích Claude làm được gì, gói miễn phí và trả phí khác nhau ra sao, cách mở cuộc trò chuyện đầu tiên, và những chỗ Claude hay sai mà người mới nên biết. Mọi tính năng và giá đều kiểm ở trang chính thức.
Working· 2026-10-04