CSSمتوسط

آبشار و Specificity در CSS: چرا استایل‌هایتان اعمال نمی‌شوند

وقتی دو قاعدهٔ CSS با هم مخالف‌اند، مرورگر برای انتخاب برنده از مجموعه‌ای از قواعدِ دقیق پیروی می‌کند. یاد بگیرید آبشار چطور تصمیم می‌گیرد، specificity را با دست حساب کنید و دیگر سراغ !important نروید.

پنج قاعدهٔ CSS که برای یک دکمه با هم رقابت می‌کنند و معیارهای مرورگر تعیین می‌کنند کدام برنده است
در این صفحه

یک قاعدهٔ CSS می‌نویسید. صفحه را دوباره بارگذاری می‌کنید. هیچ چیزی عوض نمی‌شود. DevTools را باز می‌کنید و آنجاست، خط خورده: قاعدهٔ دیگری در جایی برنده شده. پس یک کلاس اضافه می‌کنید، بعد یک ID، بعد !important، و شش ماه بعد هیچ‌کس جرئت نمی‌کند به استایل‌شیت دست بزند.

اینکه کدام قاعده برنده می‌شود هیچ چیزِ تصادفی‌ای ندارد. وقتی چند اعلان (declaration) یک ویژگی را روی یک عنصر تنظیم می‌کنند، مرورگر آبشار (cascade) را اجرا می‌کند: یک توالیِ ثابت از معیارهای تعیین برنده. این توالی را یاد بگیرید تا هر بار برنده را پیش‌بینی کنید و CSSی بنویسید که اصلاً به !important نیاز نداشته باشد. (اگر هنوز با چیدمان راحت نیستید، Flexbox یا Grid در CSS شروع خوبی است.)

آبشار، به ترتیب#

مرورگر برای هر ویژگیِ هر عنصر، همهٔ اعلان‌هایی را که اعمال می‌شوند جمع می‌کند و بعد آن‌ها را به این ترتیب مقایسه می‌کند. اولین مرحله‌ای که تفاوتی پیدا کند تصمیم می‌گیرد؛ مرحله‌های بعدی فقط تساوی را می‌شکنند.

⁧برنده⁩⁧بازنده⁩ 1 ⁧منشأ و اهمیت⁩ !important > ⁧معمولی⁩ ⁧مساوی⁩ 2 ⁧استایل درون‌خطی⁩ style="…" > ⁧هر سلکتوری⁩ ⁧مساوی⁩ 3 ⁧لایهٔ آبشار⁩ ⁧لایهٔ بعدی⁩ > ⁧لایهٔ قبلی⁩ ⁧مساوی⁩ 4 Specificity (1, 0, 0) > (0, 9, 9) ⁧مساوی⁩ 5 ⁧ترتیب ظاهر شدن⁩ ⁧آخری⁩ > ⁧قبلی‌ها⁩ ⁧اولین مرحله‌ای که تفاوتی پیدا کند تصمیم می‌گیرد؛ مرحله‌های بعدی فقط تساوی را می‌شکنند⁩
آبشار اعلان‌ها را مرحله‌به‌مرحله مقایسه می‌کند: منشأ و اهمیت، استایل درون‌خطی، لایهٔ آبشار، specificity و در آخر ترتیب ظاهر شدن؛ اولین مرحله‌ای که تفاوتی داشته باشد برنده را انتخاب می‌کند
  1. منشأ و اهمیت. استایل‌شیت‌های شما بر استایل‌های پیش‌فرضِ مرورگر غلبه می‌کنند. اعلانی که با !important علامت خورده بر همهٔ اعلان‌های معمولی غلبه می‌کند.
  2. استایل‌های درون‌خطی. ویژگیِ style="…" بر هر سلکتوری در استایل‌شیت غلبه می‌کند.
  3. لایه‌های آبشار. اگر از @layer استفاده کنید، استایل‌های لایه‌های بعدی بر لایه‌های قبلی غلبه می‌کنند، سلکتورهایشان هر چه باشد.
  4. Specificity. سلکتورِ دقیق‌تر برنده است.
  5. ترتیب ظاهر شدن. اگر همه‌چیزِ دیگر برابر باشد، اعلانی که آخر می‌آید برنده است.

بیشترِ غافلگیری‌های روزمره در مرحله‌های ۴ و ۵ اتفاق می‌افتند، پس از همان‌جا شروع کنیم.

محاسبهٔ specificity#

Specificity امتیازی است که از سه عدد ساخته می‌شود و معمولاً به شکل (A, B, C) نوشته می‌شود:

  • A: تعداد سلکتورهای ID (#header)
  • B: تعداد سلکتورهای کلاس (.card)، سلکتورهای attribute ([type="email"]) و شبه‌کلاس‌ها (:hover، :focus، :nth-child())
  • C: تعداد سلکتورهای نوع (p، button) و شبه‌عنصرها (::before)

سلکتورِ همگانیِ * و ترکیب‌کننده‌ها (، >، +، ~) هیچ امتیازی ندارند.

سلکتور Specificity
p (0, 0, 1)
.card p (0, 1, 1)
.card .title (0, 2, 0)
a:hover (0, 1, 1)
input[type="email"] (0, 1, 1)
ul li::before (0, 0, 3)
#sidebar a (1, 0, 1)
#nav .item a:hover (1, 2, 1)
* (0, 0, 0)

برای مقایسهٔ دو سلکتور، اول A را مقایسه کنید. فقط اگر A برابر بود سراغ B بروید، و فقط اگر B برابر بود سراغ C. بین ستون‌ها هیچ «انتقالی» وجود ندارد: یک ID یعنی (1, 0, 0) بر سلکتوری با بیست کلاس یعنی (0, 20, 0) غلبه می‌کند.

این یک غافلگیریِ کلاسیک است:

CSS
#main .button { background: gray; }   /* (1, 1, 0) */
.button.primary { background: blue; } /* (0, 2, 0) */
HTML
<div id="main">
  <button class="button primary">Save</button>   <!-- gray! -->
</div>

دکمهٔ primary خاکستری است، با اینکه .button.primary از نظر یک آدم دقیق‌تر به نظر می‌رسد و بعدتر هم آمده. ID در ستون A برنده می‌شود و کار هرگز به مرحلهٔ ۵ نمی‌رسد.

بگذارید DevTools حساب کند

در مرورگرهای مبتنی بر Chromium، نشانگر ماوس را روی یک سلکتور در پنل Styles نگه دارید تا specificity آن را ببینید. زبانهٔ Computed مقدار نهاییِ هر ویژگی را نشان می‌دهد و وقتی بازش کنید، همهٔ قاعده‌هایی را که سعی کرده‌اند آن را تنظیم کنند، با برنده در ابتدا.

شبه‌کلاس‌های :is()، :not()، :has() و :where()#

این شبه‌کلاس‌ها فهرستی از سلکتورها می‌گیرند و امتیازشان طور دیگری حساب می‌شود:

  • :is()، :not() و :has() specificity دقیق‌ترین آرگومانشان را می‌گیرند. :is(#nav, .menu) a امتیاز (1, 0, 1) دارد، حتی وقتی از طریق .menu منطبق می‌شود.
  • :where() همیشه specificity صفر دارد، هر چه داخلش باشد.

همین :where() را برای پیش‌فرض‌هایی که باید راحت بازنویسی شوند عالی می‌کند:

CSS
/* (0, 0, 1): any later class beats it */
:where(.prose) a { color: royalblue; }

/* (0, 1, 1): would beat most component styles, which is usually not what you want */
.prose a { color: royalblue; }

ارث‌بری همان آبشار نیست#

بعضی ویژگی‌ها، بیشترشان مربوط به متن مثل color، font-family و line-height، به ارث می‌رسند: اگر هیچ چیزی آن‌ها را روی یک عنصر تنظیم نکند، عنصر مقدارِ والدش را می‌گیرد. بقیه، مثل margin، border و background، به ارث نمی‌رسند.

تله اینجاست: یک مقدارِ به‌ارث‌رسیده همیشه به هر قاعده‌ای که مستقیماً خودِ عنصر را هدف می‌گیرد می‌بازد، سلکتورِ آن قاعده هر قدر هم ضعیف باشد.

CSS
#article { color: darkred; }   /* (1, 0, 0), but applies to the div */
p { color: black; }            /* (0, 0, 1), applies to the p itself */
HTML
<div id="article">
  <p>This text is black, not dark red.</p>
</div>

Specificity فقط قاعده‌هایی را مقایسه می‌کند که با یک عنصرِ واحد منطبق می‌شوند. قاعدهٔ #article با div منطبق می‌شود؛ پاراگراف فقط از آن ارث می‌برد، و ارث‌بری فقط وقتی وارد عمل می‌شود که هیچ قاعده‌ای color را روی خودِ پاراگراف تنظیم نکند.

چهار کلیدواژه به شما اجازه می‌دهند این را روی هر ویژگی‌ای صریحاً کنترل کنید:

کلیدواژه معنی
inherit مقدارِ والد را بگیر
initial از مقدارِ پیش‌فرضِ ویژگی در مشخصات CSS استفاده کن
unset برای ویژگی‌های ارثی مثل inherit، برای بقیه مثل initial
revert به همان چیزی برگرد که استایل‌شیتِ خودِ مرورگر تنظیم می‌کند

کلیدواژهٔ !important و استایل‌های درون‌خطی#

!important در مرحلهٔ ۱ از صف جلو می‌زند، حتی قبل از اینکه به specificity نگاه شود. دقیقاً به همین دلیل دردسر می‌سازد: تنها راهِ بازنویسیِ یک اعلانِ !important یک اعلانِ !important دیگر است که در مرحله‌های بعدی برنده شود، و این‌طور مسابقهٔ تسلیحاتی شروع می‌شود.

ویژگی‌های style درون‌خطی (مرحلهٔ ۲) بر هر سلکتوری غلبه می‌کنند، اما به !important در استایل‌شیت می‌بازند.

کلیدواژهٔ !important را برای کلاس‌های کمکی نگه دارید

یک استفادهٔ معقول، یک کلاسِ کمکیِ کوچک است که باید همیشه برنده شود، مثل .hidden { display: none !important; }. استفاده از آن برای حل یک مشکلِ specificity فقط مشکل را تا دفعهٔ بعد پنهان می‌کند.

لایه‌های آبشار: specificity تحت کنترل#

لایه‌های آبشار (@layer، که از سال ۲۰۲۲ در همهٔ مرورگرهای اصلی پشتیبانی می‌شود) یک مرحله بالاتر از specificity اضافه می‌کنند. ترتیب لایه‌هایتان را یک بار اعلام می‌کنید و از آن به بعد لایهٔ بعدی بر لایهٔ قبلی غلبه می‌کند، سلکتورها هر چه باشند:

CSS
@layer reset, vendor, components, utilities;

@layer vendor {
  #app .btn { background: gray; }       /* (1, 1, 0) */
}

@layer components {
  .btn { background: royalblue; }       /* (0, 1, 0), and it wins */
}

در عبارتِ @layer، لایهٔ components بعد از vendor آمده؛ پس .btn آن با وجود specificity خیلی کمتر برنده می‌شود. این برای CSS کتابخانه‌های بیرونی ایده‌آل است: آن را در یک لایهٔ اولیه بپیچید (@import url("library.css") layer(vendor);) تا استایل‌های خودتان بدون ترفندهای specificity بر آن غلبه کنند.

دو جزئیاتِ ارزشمند: استایل‌هایی که در هیچ لایه‌ای نیستند بر همهٔ استایل‌های لایه‌دار غلبه می‌کنند، و برای اعلان‌های !important ترتیب لایه‌ها برعکس می‌شود؛ یعنی !important یک لایهٔ قبلی بر !important لایهٔ بعدی غلبه می‌کند.

عادت‌هایی که جلوی جنگِ specificity را می‌گیرند#

  • با کلاس استایل بدهید، نه با ID. IDها را برای لینک‌های داخلی و جاوااسکریپت نگه دارید.
  • سلکتورها را کوتاه و تخت نگه دارید. .card-title به‌جای .sidebar .card > h2.title. سلکتورهای تخت specificity مشابهی دارند، پس ترتیبِ کد و لایه‌ها تصمیم می‌گیرند، و استدلال دربارهٔ این‌ها آسان است.
  • پیش‌فرض‌ها و ریست‌ها را در :where() بپیچید تا بازنویسی‌شان ساده باشد.
  • از @layer استفاده کنید تا استایل‌های پایه و کتابخانه‌های بیرونی زیرِ کامپوننت‌هایتان قرار بگیرند.
  • وقتی قاعده‌ای اعمال نمی‌شود، اول DevTools را نگاه کنید: خط خورده است (در آبشار باخته)، نیست (سلکتور منطبق نمی‌شود)، یا با هشدار نشان داده شده (مقدارِ نامعتبر)؟

جمع‌بندی#

  • آبشار برنده را به ترتیبی ثابت انتخاب می‌کند: منشأ و !important، استایل‌های درون‌خطی، لایه‌ها، specificity و بعد ترتیب ظاهر شدن.
  • Specificity یعنی (IDها، کلاس‌ها/attributeها/شبه‌کلاس‌ها، عنصرها/شبه‌عنصرها) که ستون‌به‌ستون و بدون انتقال مقایسه می‌شوند.
  • :is()، :not() و :has() امتیازِ قوی‌ترین آرگومانشان را می‌گیرند؛ :where() صفر اضافه می‌کند.
  • مقدارهای به‌ارث‌رسیده به هر قاعده‌ای که مستقیماً خودِ عنصر را هدف بگیرد می‌بازند.
  • سلکتورهای کلاسیِ تخت، :where() و @layer را به IDها و !important ترجیح دهید.