توسعه‌دهندگان عاشق پیروزی‌های سریع هستند. وقتی تیکت می‌گوید «حالت تاریک (dark mode) را اضافه کن»، مسیر کمترین مقاومت کاملاً بدیهی به نظر می‌رسد: نوشتن light.css و dark.css و جابه‌جایی بین آن‌ها. این روش تمیز به نظر می‌رسد و سریع هم عرضه می‌شود. برای یک پروژه جانبی کوچک با سه کامپوننت، شاید حتی جواب بدهد. اما به محض اینکه اپلیکیشن شما از چند ماژول فراتر رفت، آن فایل دوم دیگر یک دارایی نیست، بلکه به بدهی‌ای تبدیل می‌شود که باید آن را به صورت دوگانه نگهداری کنید.

تله‌ی دو فایلی

منطق در نگاه اول درست به نظر می‌رسد. جداسازی دغدغه‌ها (Separation of concerns)، درست است؟ موارد روشن این طرف، موارد تاریک آن طرف. دو بافر در ادیتور خود باز می‌کنید. استایل‌های کارت را از فایل روشن به فایل تاریک کپی می‌کنید، #ffffff را با #1a1a1a جایگزین می‌کنید و کار را تمام می‌کنید.

مشکل در هفته اول نیست. مشکل در ماه ششم است؛ زمانی که طراح درخواست شعاع حاشیه (border radius) کمی متفاوت برای دکمه اصلی را دارد، یا زمانی که تیم محصول یک حالت هشدار جدید برای فرم پرداخت می‌خواهد. شما استایل‌شیت روشن را به‌روز می‌کنید. استایل‌شیت تاریک را هم با چشم برانداز می‌کنید. شاید یادتان باشد تغییر را کپی کنید، شاید هم نه. این شکاف همان جایی است که کیفیت از بین می‌رود. شما دیگر یک رابط کاربری را نگهداری نمی‌کنید؛ بلکه دارید دو رابط کاربری موازی را نگهداری می‌کنید که از قضا از یک ساختار HTML مشترک استفاده می‌کنند.

انحراف تم اجتناب‌ناپذیر است

این شکاف نامی دارد که تیم‌های فرانت‌اند کم‌کم در حال شناخت آن هستند: انحراف تم (theme drift). این اتفاق زمانی رخ می‌دهد که دو استایل‌شیت شما با سرعت‌های متفاوتی تکامل می‌یابند. یک تنظیم فاصله (padding) در اینجا، یک تغییر سایه در آنجا. فایل تاریک به خواهر و برادر نادیده گرفته شده تبدیل می‌شود. یا بدتر از آن، به منبعی برای ترس تبدیل می‌شود. توسعه‌دهندگان شروع به اجتناب از تغییرات می‌کنند، چون دست زدن به یک تم به معنای جست‌وجو در فایلی دیگر برای تکرار همان کار است.

بار شناختی (cognitive overhead) به سرعت افزایش می‌یابد. شما می‌خواستید CSS را یک بار بنویسید، اما در عوض آن را دو بار نوشتید و حالا هر بار که سیستم طراحی تغییر می‌کند، باید بهره‌ی آن بدهی را بپردازید. آیکون‌ها در حالت تاریک نامیزان می‌شوند چون کسی یک flex gap را در فایل روشن به‌روز کرده و فراموش کرده آن را در فایل تاریک هم اعمال کند. حلقه‌های تمرکز (focus rings) ناپدید می‌شوند چون یک قانون دسترسی‌پذیری (accessibility) جدید فقط در یک شیت اعمال شده است. رابط کاربری (UI) فقط ظاهر بدی پیدا نمی‌کند، بلکه شروع به خراب به نظر رسیدن می‌کند.

از توکن‌های معنایی استفاده کنید

راه حل، ابزار مقایسه (diff tool) بهتر یا بازبینی کد (code review) سخت‌گیرانه‌تر نیست. راه حل، روش متفاوتی برای فکر کردن به رنگ‌هاست. سازماندهی استایل‌ها را بر اساس ظاهر فیزیکی متوقف کنید و سازماندهی آن‌ها را بر اساس هدف شروع کنید. اینجاست که توکن‌های معنایی (semantic tokens) وارد عمل می‌شوند.

به جای اختصاص دادن یک پس‌زمینه سفید به یک کارت، یک پس‌زمینه سطحی (surface background) به آن اختصاص دهید. به جای انتخاب بین سیاه و سفیدِ مایل به خاکستری برای متن، یک رنگ متن انتخاب کنید. کامپوننت نمی‌داند یا اهمیتی نمی‌دهد که کاربر حالت روشن را ترجیح می‌دهد یا تاریک. او صرفاً توکنی را درخواست می‌کند که با وظیفه‌اش مطابقت دارد.

یک دکمه استاندارد را در نظر بگیرید. در دنیای دو فایلی، .btn در استایل‌شیت روشن با پس‌زمینه سفید و حاشیه تیره زندگی می‌کند. همزاد آن در استایل‌شیت تاریک با پس‌زمینه‌ای نزدیک به سیاه و حاشیه‌ای روشن‌تر زندگی می‌کند. این یعنی دو برابر کد برای یک دکمه. با توکن‌ها، .btn تنها یک اعلان (declaration) دارد: پس‌زمینه var(--color-surface-secondary) است و حاشیه var(--color-border-default). خودِ مقادیر در ریشه (root) قرار دارند. وقتی سایت در حالت روشن است، --color-surface-secondary به چیزی مثل #f8f9fa تبدیل می‌شود. در حالت تاریک، همان توکن به #2d2d2d تبدیل می‌شود. کامپوننت دکمه هرگز تغییر نمی‌کند؛ فقط داده‌های زیر آن تغییر می‌کنند.

این تمایز بین ساختار و داده، ظریف اما قدرتمند است. کامپوننت کارت شما چیدمان، فاصله، تایپوگرافی و ارتفاع (elevation) را یک بار تعریف می‌کند. لایه تم شما پالت رنگی را تعریف می‌کند. این جداسازی دقیقاً همان چیزی است که برای آن ویژگی‌های سفارشی CSS (CSS custom properties) ساخته شده‌اند.

معماری چگونه تغییر می‌کند

این رویکرد، نحوه نوشتن استایل‌ها را به طور بنیادی بازسازی می‌کند.

روش قدیمی معمولاً به این شکل است:

  • یک استایل‌شیت کارت روشن که padding، radius، background، رنگ متن و shadow را تعریف می‌کند.
  • یک استایل‌شیت کارت تاریک که بیشتر همان ویژگی‌ها را فقط برای تغییر رنگ‌ها دوباره تعریف می‌کند.
  • یک لایه منطقی که تصمیم می‌گیرد کدام استایل‌شیت بارگذاری شود یا کدام کلاس روی body تغییر کند.

روش جدید به این شکل است:

  • یک استایل‌شیت کارت که چیدمان را تعریف کرده و توکن‌های معنایی را اختصاص می‌دهد.
  • یک فایل تم که معنای آن توکن‌ها را در بافت روشن تعریف می‌کند.
  • یک فایل تم، یا صرفاً یک بلوک در همان فایل، که معنای آن توکن‌ها را در بافت تاریک تعریف می‌کند.
  • یک جابه‌جایی تک‌اتریبیوتی که بدون دست زدن به لایه کامپوننت، لایه مقادیر را تغییر می‌دهد.

شما ساختار را پایدار نگه می‌دارید. شما فقط داده‌ها را تغییر می‌دهید. وقتی طراح می‌خواهد تم سومی را معرفی کند، مثلاً یک حالت کنتراست بالا یا یک نسخه آبی تیره (midnight blue)، نیازی نیست کارت را دوباره بنویسید. فقط یک تخصیص دیگر به نقشه توکن (token map) اضافه می‌کنید. کامپوننت ساده و بی‌دردسر باقی می‌ماند. آن همچنان به یک رنگ سطح (surface color) نیاز دارد. تم به آن می‌گوید که از چه رنگ سطحی استفاده کند.

سوئیچ با استفاده از ویژگی داده (Data Attribute)

پیاده‌سازی می‌تواند ساده و خوانا باقی بماند. یک ویژگی داده (data attribute) به تگ HTML خود اعمال کنید، چیزی شبیه به data-theme="dark"، و اجازه دهید تعاریف توکن شما تحت آن محدود (scope) شوند.

مقادیر پیش‌فرض خود را در :root برای تجربه حالت روشن (light) تنظیم کنید تا صفحه قبل از اجرای جاوااسکریپت به‌درستی رندر شود. سپس مقادیر توکن را در [data-theme="dark"] بازنویسی (override) کنید. یک اسکریپت کوچک کلیک روی دکمه تغییر وضعیت (toggle) را زیر نظر می‌گیرد، ویژگی را به‌روز می‌کند و هر کامپوننت در صفحه فوراً واکنش نشان می‌دهد. بدون دستکاری بی‌مورد کلاس‌ها در تک‌تک المان‌ها. بدون وارد کردن یک استایل‌شیت کاملاً مجزا در میان رندر. مرورگر از قبل متغیرها را در حافظه دارد؛ فقط با مقادیر جدید دوباره ترسیم (repaint) می‌کند.

این کار کد شما را از نظر کاربردی بسیار تمیز نگه می‌دارد. مجبور نیستید برای پیدا کردن تمام نمونه‌های .card در دو دایرکتوری مختلف جستجو (grep) کنید. لازم نیست نگران جنگ ویژگی‌ها (specificity wars) بین کلاس‌های تم رقیب که روی یک گره (node) انباشته شده‌اند باشید. HTML شما خوانا باقی می‌ماند. CSS شما متمرکز و قابل جستجو می‌ماند.

موضوع بر سر مقادیر است، نه نسخه‌ها

حالت تاریک (Dark mode) درباره مقادیر است. این نسخه دوم رابط کاربری (UI) شما نیست. گوشه‌های کارت شما در شب گردتر نمی‌شوند. گرید (grid) شما به شکل دیگری فرو نمی‌ریزد. مقیاس تایپوگرافی (type scale) شما به ریتم جدیدی نیاز ندارد. فقط رنگ‌ها تغییر می‌کنند و گاهی سایه‌ها کمی عمیق‌تر می‌شوند. برخورد با حالت تاریک به عنوان یک تغییر ظاهر (reskin) کامل، مهندسی بیش از حد (over-engineering) است که کابوس‌های نگهداری ایجاد می‌کند.

تیم‌هایی که این موضوع را درست درک می‌کنند، با سیستم طراحی خود مانند یک پایگاه داده رفتار می‌کنند. کامپوننت‌ها ویژگی‌ها را با نام جستجو (query) می‌کنند. تم‌ها رکوردها را ارائه می‌دهند. تغییر از حالت روشن به تاریک، تغییر یک پارامتر پرس‌وجو (query parameter) است، نه بازنویسی طرحواره (schema).

این طرز فکر است که شما را از انحراف تم (theme drift) نجات می‌دهد. یک کارت. یک دکمه. یک منبع واحد حقیقت (source of truth) برای فاصله‌گذاری و اندازه‌گذاری. پالت رنگی در یک مکان زندگی می‌کند، به صورت منطقی نگاشت شده و آماده برای هر محیطی است که کاربر ترجیح می‌دهد.

نتیجه‌گیری اصلی

اگر برای حالت‌های روشن و تاریک دو فایل CSS را نگهداری می‌کنید، شما در حال تم‌سازی (theming) نیستید، بلکه در حال کپی‌برداری (cloning) هستید. به سمت توکن‌های معنایی (semantic tokens) بروید، آن‌ها را با یک ویژگی داده در سطح ریشه محدود کنید و اجازه دهید کامپوننت‌های شما به جای کدنویسی سخت (hardcoding) ظاهر، نقش‌ها (roles) را درخواست کنند. بازنویسی (refactor) اولیه تلاش می‌طلبد، اما جایگزین آن، بازی بی‌پایانِ «زدن موش» (whack-a-mole) در میان استایل‌شیت‌های موازی است. زندگی کوتاه‌تر از آن است که یک کارت را دو بار بنویسید.