นักพัฒนาชอบผลลัพธ์ที่เห็นผลไว เมื่อตั๋วระบุว่า "เพิ่ม dark mode" เส้นทางที่ง่ายที่สุดก็ดูชัดเจน: เขียน light.css, เขียน dark.css, แล้วสลับไปมาระหว่างกัน มันดูสะอาดตาและส่งงานได้เร็ว สำหรับโปรเจกต์เล็กๆ ที่มีแค่ไม่กี่คอมโพเนนต์ มันอาจจะยังใช้งานได้ดี แต่เมื่อแอปพลิเคชันของคุณเติบโตเกินกว่าไม่กี่โมดูล ไฟล์ที่สองนั้นจะไม่ใช่สินทรัพย์อีกต่อไป แต่จะกลายเป็นภาระที่คุณต้องคอยดูแลซ้ำซ้อน
กับดักแบบสองไฟล์
ตรรกะดูเหมือนจะสมเหตุสมผลในตอนแรก การแยกส่วนความรับผิดชอบ (Separation of concerns) ใช่ไหมล่ะ? ของโทนสว่างไว้ทางนี้ ของโทนเข้มไว้ทางนั้น คุณเปิดไฟล์สองไฟล์ในเอดิเตอร์ คัดลอกสไตล์ของ card จากไฟล์ light ไปยังไฟล์ dark เปลี่ยน #ffffff เป็น #1a1a1a แล้วก็จบงาน
ปัญหามันไม่ได้อยู่ที่สัปดาห์แรก แต่มันอยู่ที่เดือนที่หก เมื่อดีไซน์เนอร์ขอให้ปรับ border radius ของปุ่มหลักเพียงเล็กน้อย หรือเมื่อทีมผลิตภัณฑ์ต้องการสถานะ warning ใหม่บนฟอร์มชำระเงิน คุณอัปเดต stylesheet ของ light คุณกวาดสายตามอง stylesheet ของ dark คุณอาจจะจำได้ว่าต้องคัดลอกการเปลี่ยนแปลงนั้นไปด้วย หรืออาจจะลืม ช่องว่างตรงนั้นแหละคือจุดที่คุณภาพพังทลาย คุณไม่ได้กำลังดูแลอินเทอร์เฟซเดียวอีกต่อไป แต่คุณกำลังดูแลอินเทอร์เฟซสองชุดที่ขนานกันซึ่งบังเอิญใช้โครงสร้าง HTML เดียวกัน
การคลาดเคลื่อนของธีม (Theme Drift) เป็นเรื่องที่หลีกเลี่ยงไม่ได้
ช่องว่างนี้มีชื่อเรียกที่ทีม frontend เริ่มรู้จักกันดี นั่นคือ theme drift มันเกิดขึ้นเมื่อ stylesheet ทั้งสองของคุณพัฒนาไปด้วยความเร็วที่ต่างกัน การปรับ padding ตรงนี้ การปรับ shadow ตรงนั้น ไฟล์ dark กลายเป็นเหมือนน้องที่ถูกลืม หรือที่แย่กว่านั้น มันกลายเป็นสิ่งที่น่ากลัว นักพัฒนาเริ่มหลีกเลี่ยงการเปลี่ยนแปลง เพราะการแตะต้องธีมหนึ่งหมายถึงต้องไปไล่หาในอีกไฟล์หนึ่งเพื่อทำซ้ำงานเดิม
ภาระทางความคิด (Cognitive overhead) จะสะสมอย่างรวดเร็ว คุณต้องการเขียน CSS เพียงครั้งเดียว แต่คุณกลับต้องเขียนมันสองครั้ง และตอนนี้คุณกำลังจ่ายดอกเบี้ยให้กับหนี้ก้อนนั้นทุกครั้งที่ design system เปลี่ยนแปลง ไอคอนวางไม่ตรงใน dark mode เพราะมีคนไปอัปเดต flex gap ในไฟล์ light แล้วลืมทำแบบเดียวกันในไฟล์ dark Focus ring หายไปเพราะกฎการเข้าถึง (accessibility) ใหม่ถูกใส่ไว้ในไฟล์เดียวเท่านั้น UI ไม่ได้แค่ดูผิดพลาด แต่มันเริ่มให้ความรู้สึกเหมือนระบบพัง
ใช้ Semantic Tokens
วิธีแก้ไม่ใช่เครื่องมือ diff ที่ดีกว่าหรือการทำ code review ที่เข้มงวดขึ้น แต่วิธีแก้คือการเปลี่ยนวิธีคิดเกี่ยวกับสี เลิกจัดระเบียบสไตล์ตามรูปลักษณ์ที่เห็น และเริ่มจัดระเบียบตามวัตถุประสงค์ นี่คือจุดที่ semantic tokens เข้ามามีบทบาท
แทนที่จะกำหนดให้ card มีพื้นหลังเป็นสีขาว ให้กำหนดให้มันเป็น surface background แทนที่จะเลือกระหว่างสีดำหรือสีขาวหม่นสำหรับข้อความ ให้เลือกเป็น text color คอมโพเนนต์ไม่จำเป็นต้องรู้หรือสนใจว่าผู้ใช้ชอบแบบ light หรือ dark มันเพียงแค่เรียกใช้ token ที่ตรงกับหน้าที่ของมันเท่านั้น
ลองนึกถึงปุ่มมาตรฐาน ในโลกแบบสองไฟล์ .btn จะอยู่ใน stylesheet ของ light พร้อมพื้นหลังสีขาวและขอบสีเข้ม ฝาแฝดของมันจะอยู่ใน stylesheet ของ dark พร้อมพื้นหลังเกือบดำและขอบที่สว่างกว่า นั่นคือการเขียนโค้ดสองเท่าสำหรับปุ่มเดียว แต่เมื่อใช้ tokens .btn จะมีการประกาศเพียงครั้งเดียว: พื้นหลังคือ var(--color-surface-secondary) และขอบคือ var(--color-border-default) ตัวค่าสีจริงๆ จะอยู่ที่ root เมื่อไซต์อยู่ใน light mode --color-surface-secondary จะถูกแทนที่ด้วยค่าอย่าง #f8f9fa ใน dark mode token ตัวเดียวกันนี้จะถูกแทนที่ด้วย #2d2d2d คอมโพเนนต์ปุ่มไม่เคยเปลี่ยน มีเพียงข้อมูลที่อยู่เบื้องหลังเท่านั้นที่เปลี่ยนไป
ความแตกต่างระหว่างโครงสร้าง (structure) และข้อมูล (data) นี้ดูเหมือนจะเล็กน้อยแต่ทรงพลัง คอมโพเนนต์ card ของคุณจะกำหนด layout, spacing, typography และ elevation เพียงครั้งเดียว ส่วน theme layer จะเป็นตัวกำหนดพาเลทสี การแยกส่วนแบบนี้คือสิ่งที่ CSS custom properties ถูกสร้างขึ้นมาเพื่อสิ่งนี้โดยเฉพาะ
สถาปัตยกรรมเปลี่ยนไปอย่างไร
แนวทางนี้จะปรับโครงสร้างวิธีการเขียนสไตล์ของคุณอย่างสิ้นเชิง
แบบเดิมมักจะเป็นแบบนี้:
- Stylesheet ของ card แบบ light ที่กำหนด padding, radius, background, text color และ shadow
- Stylesheet ของ card แบบ dark ที่ต้องกำหนดคุณสมบัติเดิมเกือบทั้งหมดใหม่เพียงเพื่อสลับสี
- เลเยอร์ตรรกะ (logic layer) ที่ตัดสินใจว่าจะโหลด stylesheet ไหน หรือจะสลับ class ใดบน body
แบบใหม่จะเป็นแบบนี้:
- Stylesheet ของ card เพียงหนึ่งเดียวที่กำหนด layout และกำหนด semantic tokens
- ไฟล์ theme หนึ่งไฟล์ที่กำหนดว่า token เหล่านั้นหมายถึงอะไรในบริบทของ light mode
- ไฟล์ theme อีกหนึ่งไฟล์ (หรือเพียงแค่บล็อกในไฟล์เดียวกัน) ที่กำหนดว่า token เหล่านั้นหมายถึงอะไรในบริบทของ dark mode
- การสลับ attribute เพียงครั้งเดียวที่เปลี่ยนเลเยอร์ของค่า (value layer) โดยไม่ต้องแตะต้องเลเยอร์ของคอมโพเนนต์ (component layer)
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.
