توسعه فرانت‌اند

CSS Custom Properties: متغیرهای CSS برای طراحی پویا

رفتار واقعی Cascade و ارث متغیرهای CSS، محدودیت var و fallback، ثبت نوع با @property و نمونه مستقل Theme و Scope محلی.

1404/11/03 3 دقیقه مطالعه 2,871 بازدید

وقتی رنگ متن یک کارت در چند فایل تکرار می‌شود، تغییر آن به پیدا کردن تمام استفاده‌ها وابسته می‌شود. CSS Custom Properties می‌توانند مقدار مشترک را با نام معنادار معرفی کنند. اما ارزش آن‌ها فقط کاهش تکرار نیست؛ با Scope و Cascade می‌توانید یک component را در زمینه‌های مختلف تنظیم کنید، بدون اینکه برای هر حالت کل قواعد آن را دوباره بنویسید.

متغیرهای CSS مانند متغیر زمان build در Sass رفتار نمی‌کنند. مقدارشان در سند و در رابطه با عنصر محاسبه می‌شود و می‌تواند از والد به فرزند برسد. این ویژگی برای Theme و تنظیم component مفید است، ولی اگر نام و دامنه روشن نباشد، تغییر یک مقدار می‌تواند بخش‌های غیرمنتظره‌ای را تحت تأثیر قرار دهد. قرارداد متغیر را همان‌قدر جدی بگیرید که selector را می‌گیرید.

نام مقدار باید نقش آن را توضیح دهد

نام‌هایی مانند --surface و --text نقش رنگ را بیان می‌کنند. --blue فقط رنگ فعلی را می‌گوید و در Theme تاریک شاید دیگر توصیف مناسبی نباشد. می‌توانید لایه مقدار پایه و لایه نقش را جدا تعریف کنید. برای مثال، یک palette ممکن است رنگ‌های خام داشته باشد و component از نقش‌هایی مانند --button-background استفاده کند. این دو لایه باید ارتباط قابل پیگیری داشته باشند.

همه مقدارها نیاز به token عمومی ندارند. اگر اندازه‌ای فقط جزئی از implementation یک component است، Scope محلی مناسب‌تر است. قرار دادن صدها متغیر در :root ممکن است وابستگی عمومی بسازد که بعداً تغییرش دشوار شود. مشخص کنید کدام متغیر ورودی قابل تنظیم component است و کدام فقط جزئیات داخلی آن است.

نام Custom Property به حروف بزرگ و کوچک حساس است. --space با --Space یک نام نیست. یکدست بودن نام‌گذاری به یافتن مقدار کمک می‌کند. از نام‌های مبهم مثل --x برای قرارداد عمومی دور بمانید، مگر واقعاً coordinate مشخصی را نشان می‌دهد. نام نباید لازم کند خواننده برای فهم نقش، همه فایل‌های استفاده‌کننده را باز کند.

Scope از عنصر و رابطه والد می‌آید

راهنمای Custom Properties در MDN نحوه تعریف با دو خط تیره، استفاده با var و رفتار Cascade را توضیح می‌دهد. متغیر معمولی تعریف‌شده با این روش از والد ارث می‌رسد. تغییر مقدار روی یک container می‌تواند فرزندانش را تنظیم کند، ولی خواهر یا برادر همان container خودکار همان مقدار را نمی‌گیرد.

قرار دادن مقدار در :root یک قرارداد عمومی می‌سازد، نه یک storage مستقل خارج از سند. selectorهای دیگر می‌توانند مقدار را برای بخش خود override کنند. ترتیب، specificity و قواعد Cascade همچنان اثر دارند. اگر Theme و component هر دو مقدار یک نام را تغییر می‌دهند، رابطه‌شان را روشن کنید تا نتیجه فقط به ترتیب تصادفی import فایل‌ها وابسته نباشد.

متغیر می‌تواند به متغیر دیگر ارجاع بدهد، اما محل محاسبه اهمیت دارد. Alias ارث‌رسیده را مانند یک formula عمومی که در هر فرزند دوباره با ورودی تازه حل می‌شود تصور نکنید. اگر component باید از مقدار محلی خاصی استفاده کند، ارجاع مربوط را در همان Scope تعریف کنید. چرخه میان متغیرها هم مقدار معتبر تولید نمی‌کند و باید در طراحی tokenها حذف شود.

Fallback با هر نوع مقدار نامعتبر یکسان برخورد نمی‌کند

var(--value, fallback) برای نبود یا مقدار guaranteed-invalid می‌تواند fallback بدهد. اگر --value موجود است ولی برای property مصرف‌کننده مناسب نیست، fallback الزاماً انتخاب نمی‌شود. برای مثال، مقدار متنی نامناسب برای width ممکن است declaration را در زمان محاسبه نامعتبر کند. وجود یک رشته در متغیر به معنی سازگار بودن آن با هر property نیست.

همین تفاوت می‌تواند در debug گمراه‌کننده باشد. یک declaration قبلی را همیشه به عنوان پشتیبان declaration دارای var فرض نکنید؛ نامعتبر شدن در computed-value time با رد شدن ساده هنگام parse فرق دارد. مقدار نهایی ممکن است به رفتار unset و ارث یا initial برسد. نوع و کاربرد متغیر باید در قرارداد component مشخص باشد.

Fallback می‌تواند به var دیگری ارجاع دهد، ولی زنجیره طولانی یافتن منبع را سخت می‌کند. اگر هر component چندین مقدار نامرتبط را به ترتیب امتحان می‌کند، شاید API طراحی روشن نیست. یک مقدار اولیه مناسب و Scope محدود معمولاً از شبکه‌ای از fallbackهای مبهم قابل نگهداری‌تر است. fallback جای اصلاح مقدار نادرست ورودی را نمی‌گیرد.

@property نوع و ارث را ثبت می‌کند

راهنمای @property در MDN ثبت syntax، inherits و initial-value را توضیح می‌دهد. برای یک مقدار طول می‌توانید نوع length و مقدار اولیه مستقل تعریف کنید. ثبت، قواعد متغیر را تغییر می‌دهد؛ نام ثبت‌شده دیگر مثل متغیر بدون نوع با هر رشته‌ای رفتار نمی‌کند. مشخص کنید این تغییر برای کدام مصرف‌کننده‌ها لازم است.

inherits:false یعنی مقدار از والد به شیوه معمول ارث نمی‌رسد. در نمونه، --card-space ورودی محلی padding کارت است و مقدار اولیه مشخص دارد. رنگ‌های نقش‌دار همچنان با متغیرهای معمولی از زمینه می‌آیند. این تفکیک نشان می‌دهد همه tokenها نباید یک سیاست ارث داشته باشند. تنظیم container برای فاصله کارت ثبت‌شده نیاز به تعریف در Scope مناسب دارد.

ثبت نوع می‌تواند interpolation مناسب برای بعضی animationها فراهم کند. بدون ثبت، تغییر مقدار Custom Property معمولاً مانند مقدار typed قابل interpolation فرض نمی‌شود. از این امکان برای حرکت ضروری و محدود استفاده کنید و ترجیح کاهش حرکت را رعایت کنید. ثبت @property به تنهایی دلیل اضافه کردن animation به همه تغییرهای Theme نیست.

نمونه کامل با Theme و Scope محلی

کد زیر یک فایل HTML مستقل است. آن را با نام index.html ذخیره و باز کنید. دو کارت از قواعد مشترک استفاده می‌کنند و کارت دوم رنگ accent محلی دارد. تنظیم Theme با checkbox و selector :has انجام می‌شود؛ در محیط فاقد این selector، حالت روشن پایه همچنان محتوای خوانا دارد. dependency، تصویر و JavaScript لازم نیست.

<!doctype html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
  <title>Scope متغیرهای CSS</title>
  <style>
    @property --card-space { syntax:"<length>"; inherits:false; initial-value:1rem; }
    * { box-sizing:border-box; }
    body { margin:0; font:1rem/1.9 system-ui,sans-serif; }
    .demo { --background:#f4f6f9; --surface:#fff; --text:#17212b; --border:#64748b; --accent:#164ca4; --on-accent:#fff; min-block-size:100vh; background:var(--background); color:var(--text); padding:2rem 1rem; }
    .content { max-inline-size:42rem; margin-inline:auto; }
    .demo:has(#dark:checked) { --background:#111827; --surface:#1f2937; --text:#f3f4f6; --border:#9ca3af; --accent:#93c5fd; --on-accent:#111827; color-scheme:dark; }
    .card { --card-space:1.25rem; padding:var(--card-space); margin-block:calc(var(--card-space) * 1.5); border:1px solid var(--border); border-radius:.5rem; background:var(--surface); }
    .card.special { --accent:#8b1d54; --on-accent:#fff; --card-space:1.5rem; }
    .card a { display:inline-block; padding:.4rem .8rem; background:var(--accent); color:var(--on-accent); }
    :focus-visible { outline:3px solid var(--accent,#164ca4); outline-offset:4px; }
  </style>
</head>
<body>
  <main class="demo">
    <div class="content">
      <h1>متغیرهای مشترک و محلی</h1>
      <label><input id="dark" type="checkbox">حالت تاریک این نمونه</label>
      <section class="card"><h2>کارت معمولی</h2><p>رنگ‌ها از زمینه ارث می‌رسند و فاصله روی کارت تعریف می‌شود.</p><a href="#explanation">خواندن توضیح</a></section>
      <section class="card special"><h2>کارت با accent محلی</h2><p>این کارت جفت رنگ accent و متن روی آن را override می‌کند.</p><a href="#explanation">خواندن توضیح</a></section>
      <p id="explanation">انتخاب Theme در این فایل ذخیره نمی‌شود. مثال فقط Scope و Cascade را نشان می‌دهد.</p>
    </div>
  </main>
</body>
</html>

در نمونه، --card-space روی خود کارت مقدار می‌گیرد. calc از همین مقدار برای فاصله داخلی استفاده می‌کند و متغیرها در مقدار property قرار گرفته‌اند. checkbox انتخاب محلی همین صفحه را تغییر می‌دهد و بعد از بازدید دوباره حفظ آن تضمین نشده است. برای ترجیح پایدار کاربر، مقاله Dark Mode جریان کامل system و انتخاب صریح را بررسی می‌کند.

اگر @property در محیطی پشتیبانی نشود، تعریف معمولی روی .card همچنان مقدار طول را می‌دهد. نمونه به animation ثبت‌شده یا منبع خارجی وابسته نیست. Scope رنگ‌ها روشن است و کارت دوم فقط جفت accent و on-accent خودش را تغییر می‌دهد. تغییر یک رنگ بدون رنگ متن روی آن، قرارداد خوانایی دکمه را کامل نمی‌کند.

var در هر جای CSS قابل استفاده نیست

Custom Property برای مقدار property است، نه نام selector یا نام property. آن را مانند template عمومی برای تولید syntax CSS نبینید. var را نمی‌توان برای مقدار شرط Media Query یا Container Query به همان روش استفاده کرد. اگر breakpoint مشترک لازم دارید، ابزار build یا ساختار طراحی دیگری را با قرارداد مناسب انتخاب کنید؛ root variable به تنهایی شرط query نمی‌سازد.

واحد را روشن نگه دارید. اگر متغیر طول است، مقدار همراه واحد بدهید. اگر عدد بدون واحد نگه می‌دارید، تبدیل آن در calc باید با قواعد آن property سازگار باشد. ترکیب رشته‌های عدد و واحد مانند بعضی templateها رفتار معمول CSS نیست. نام و مستند متغیر باید بگوید مصرف‌کننده چه نوع مقداری انتظار دارد.

در JavaScript، setProperty و getComputedStyle ابزارهای مرتبط‌اند، اما خواندن و نوشتن بی‌وقفه style می‌تواند هزینه داشته باشد. برای تغییر یک Theme، گاهی تغییر class یا attribute کافی است. منطق محصول را در صدها inline variable پخش نکنید. متغیر CSS مکانیزم نمایش است و نباید جای مدل state برنامه یا اعتبارسنجی ورودی حساس را بگیرد.

قرارداد tokenها را قابل نگهداری کنید

برای هر متغیر عمومی، نقش، نوع، Scope و مقدار اولیه را ثبت کنید. اگر component اجازه override دارد، ورودی‌ها را محدود و نام‌گذاری‌شده معرفی کنید. Container Queries برای چیدمان و Visual Hierarchy برای نقش اندازه و فاصله ادامه‌های مرتبط‌اند. token باید همین نقش را در حالت‌های مختلف حفظ کند.

هنگام تغییر Design System، رنگ، فاصله و Themeهای واقعی را با هم مرور کنید. یک متغیر با استفاده‌های متفاوت می‌تواند بیش از حد عمومی شده باشد. تقسیم آن بر اساس نقش، گاهی از overrideهای بیشتر بهتر است. هدف کاهش تعداد خطوط به هر قیمت نیست؛ هدف این است که تغییر مقدار مشخص، اثر قابل پیش‌بینی بر componentهای مشخص داشته باشد.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!