Các nhà phát triển luôn thích những thành quả nhanh chóng. Khi ticket ghi "thêm chế độ tối", con đường ít trở ngại nhất có vẻ rất rõ ràng: viết light.css, viết dark.css, và chuyển đổi giữa chúng. Cảm giác thật gọn gàng. Triển khai nhanh. Với một dự án phụ nhỏ chỉ có ba thành phần, nó thậm chí có thể ổn. Nhưng một khi ứng dụng của bạn phát triển vượt quá một vài module, tệp thứ hai đó không còn là một tài sản nữa mà trở thành một gánh nặng mà bạn phải duy trì gấp đôi.

Cái bẫy hai tệp

Logic có vẻ hợp lý ngay từ cái nhìn đầu tiên. Chia tách các mối quan tâm (separation of concerns), đúng không? Những thứ sáng ở đây, những thứ tối ở kia. Bạn mở hai buffer trong trình soạn thảo. Bạn sao chép các kiểu dáng của card từ tệp light sang tệp dark, thay #ffffff bằng #1a1a1a, và coi như xong việc.

Vấn đề không nằm ở tuần đầu tiên. Vấn đề nằm ở tháng thứ sáu, khi một nhà thiết kế yêu cầu một độ bo góc (border radius) hơi khác một chút cho nút chính, hoặc khi đội ngũ sản phẩm muốn một trạng thái cảnh báo mới trên biểu mẫu thanh toán. Bạn cập nhật stylesheet sáng. Bạn nhìn lướt qua stylesheet tối. Có thể bạn nhớ sao chép thay đổi đó sang. Có thể bạn không. Khoảng cách đó chính là nơi chất lượng bị hủy hoại. Bạn không còn duy trì một giao diện nữa. Bạn đang duy trì hai giao diện song song tình cờ dùng chung một khung HTML.

Theme drift là điều không thể tránh khỏi

Khoảng cách này có một cái tên mà các đội ngũ frontend đang bắt đầu nhận ra: theme drift. Nó xảy ra khi hai stylesheet của bạn tiến hóa với tốc độ khác nhau. Một chút điều chỉnh padding ở đây. Một chút tinh chỉnh shadow ở kia. Tệp dark trở thành "người anh em" bị bỏ rơi. Hoặc tệ hơn, nó trở thành một nguồn cơn của sự lo sợ. Các nhà phát triển bắt đầu tránh các thay đổi vì chạm vào một theme đồng nghĩa với việc phải lùng sục trong một tệp khác để sao chép công việc đó.

Gánh nặng tư duy (cognitive overhead) tăng lên nhanh chóng. Bạn muốn viết CSS một lần. Thay vào đó, bạn phải viết nó hai lần, và giờ đây bạn đang phải trả lãi cho khoản nợ đó mỗi khi hệ thống thiết kế thay đổi. Các icon bị lệch trong chế độ tối vì ai đó đã cập nhật một flex gap trong tệp light mà quên không thực hiện tương tự. Các vòng focus (focus rings) biến mất vì một quy tắc truy cập (accessibility) mới chỉ được áp dụng vào một stylesheet. Giao diện không chỉ trông sai lệch. Nó bắt đầu tạo cảm giác như bị lỗi.

Sử dụng Semantic Tokens

Giải pháp không phải là một công cụ diff tốt hơn hay quy trình review code nghiêm ngặt hơn. Giải pháp là một cách tư duy khác về màu sắc. Hãy ngừng tổ chức các kiểu dáng theo diện mạo thực tế và bắt đầu tổ chức chúng theo mục đích sử dụng. Đây chính là lúc các semantic token phát huy tác dụng.

Thay vì gán cho một card một màu nền trắng, hãy gán cho nó một màu nền bề mặt (surface background). Thay vì chọn giữa màu đen và trắng ngà cho văn bản, hãy chọn một màu văn bản (text color). Thành phần (component) không biết hoặc không quan tâm liệu người dùng thích chế độ sáng hay tối. Nó chỉ đơn giản yêu cầu token phù hợp với nhiệm vụ của nó.

Hãy nghĩ về một nút bấm tiêu chuẩn. Trong thế giới hai tệp, .btn nằm trong stylesheet sáng với nền trắng và viền tối. Bản sao của nó nằm trong stylesheet tối với nền gần như đen và viền sáng hơn. Đó là gấp đôi mã nguồn cho một nút bấm. Với các token, .btn chỉ có một khai báo duy nhất: nền là var(--color-surface-secondary) và viền là var(--color-border-default). Bản thân các giá trị nằm ở root. Khi trang web ở chế độ sáng, --color-surface-secondary sẽ được giải quyết thành một giá trị như #f8f9fa. Ở chế độ tối, cùng một token đó sẽ được giải quyết thành #2d2d2d. Thành phần nút bấm không bao giờ thay đổi. Chỉ có dữ liệu bên dưới nó thay đổi.

Sự phân biệt giữa cấu trúc và dữ liệu này tuy tinh tế nhưng rất mạnh mẽ. Thành phần card của bạn định nghĩa bố cục, khoảng cách, kiểu chữ và độ nổi (elevation) một lần duy nhất. Lớp theme của bạn định nghĩa bảng màu. Sự phân tách đó chính xác là những gì các thuộc tính tùy chỉnh CSS (CSS custom properties) được tạo ra để thực hiện.

Kiến trúc thay đổi như thế nào

Cách tiếp cận này tái cấu trúc căn bản cách bạn viết các kiểu dáng.

Cách cũ thường trông như thế này:

  • Một stylesheet card sáng định nghĩa padding, radius, background, màu văn bản và shadow.
  • Một stylesheet card tối định nghĩa lại hầu hết các thuộc tính tương tự chỉ để đảo ngược màu sắc.
  • Một lớp logic quyết định stylesheet nào sẽ được tải hoặc class nào sẽ được toggle trên thẻ body.

Cách mới trông như thế này:

  • Một stylesheet card định nghĩa bố cục và gán các semantic token.
  • Một tệp theme định nghĩa ý nghĩa của các token đó trong ngữ cảnh sáng.
  • Một tệp theme, hoặc đơn giản là một khối trong cùng một tệp, định nghĩa ý nghĩa của các token đó trong ngữ cảnh tối.
  • Một thao tác hoán đổi thuộc tính duy nhất giúp thay đổi lớp giá trị mà không cần chạm vào lớp thành phần.

You keep the setup stable. You only change the data. When the designer wants to introduce a third theme, maybe a high-contrast mode or a midnight blue variant, you do not rewrite the card. You add one more assignment to the token map. The component stays dumb and happy. It still wants a surface color. The theme tells it which surface color to use.

The Data Attribute Switch

Implementation can stay simple and readable. Apply a data attribute to your HTML tag, something like data-theme="dark", and let your token definitions scope under it.

Set your defaults on :root for the light experience so the page renders correctly before JavaScript runs. Then override the token values under [data-theme="dark"]. A tiny script watches for a toggle click, updates the attribute, and every component on the page responds instantly. No class thrashing on individual elements. No importing an entirely separate stylesheet mid-render. The browser already has the variables in memory; it just repaints with new values.

This keeps your code clean in a very practical sense. You do not have to grep across two directories to find every instance of .card. You do not have to worry about specificity wars between competing theme classes stacked on the same node. Your HTML stays readable. Your CSS stays centralized and searchable.

It Is About Values, Not Versions

Dark mode is about values. It is not a second version of your UI. The corners of your card do not get rounder at night. Your grid does not collapse into a different shape. Your type scale does not need a new rhythm. Only the colors shift, and sometimes the shadows breathe a little deeper. Treating darkness as a full reskin is over-engineering that creates maintenance nightmares.

The teams that get this right treat their design system like a database. Components query for properties by name. Themes provide the records. Switching from light to dark is a query parameter change, not a schema rewrite.

That mindset is what saves you from theme drift. One card. One button. One source of truth for spacing and sizing. The palette lives in one place, logically mapped, ready for whatever environment the user prefers.

The Real Takeaway

If you are maintaining two CSS files for light and dark, you are not theming. You are cloning. Move to semantic tokens, scope them with a root-level data attribute, and let your components ask for roles instead of hardcoding appearances. The initial refactor takes effort, but the alternative is an endless game of whack-a-mole across parallel stylesheets. Life is too short to write the same card twice.