تصویر شاخص نوشته: چرا این CSS اعمال نمی‌شود؟ cascade، specificity و inheritance

چرا این CSS اعمال نمی‌شود؟ cascade، specificity و inheritance

۱۱ مرداد ۱۴۰۵ مبانی وب

یک قاعده نوشته‌ای، ذخیره کرده‌ای، صفحه را تازه کرده‌ای — و رنگ همان رنگِ قبلی است. نه خطایی، نه هشداری، نه هیچ نشانه‌ای از اینکه چه چیزی غلط است.

تقریباً همه در همین نقطه یک کار می‌کنند: یک !important می‌چسبانند ته خط. کار هم می‌کند. مشکل این است که چیزی یاد نگرفتی، و همان جنگ فردا یک طبقه بالاتر برمی‌گردد — با یک ابزارِ کمتر در دستت.

جای این رفلکس یک مدل بگذار که بشود با آن فکر کرد. مرورگر وقتی چند اعلان برای یک property روی یک عنصر می‌بیند، شیر یا خط نمی‌اندازد؛ یک حذفِ مرحله‌به‌مرحله با ترتیبِ دقیق اجرا می‌کند. اگر آن ترتیب را بدانی، قبل از نگاه‌کردن به صفحه می‌دانی کدام رنگ برنده است.

یک چکِ صفر: اگر هیچ‌کدام از قاعده‌هایت در پنل Styles دیده نمی‌شود، مشکل از cascade نیست — فایل وصل نشده. مسیرِ href در <link> را چک کن.

cascade: مرورگر چطور برنده را انتخاب می‌کند

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

  1. origin و importance. استایل از سه جا می‌آید: خودِ مرورگر (پیش‌فرض‌هایش)، کاربر، و تو (author). قاعدهٔ عادیِ تو از پیش‌فرضِ مرورگر قوی‌تر است، بدونِ هیچ شمارشی. !important هم همین‌جا تصمیم گرفته می‌شود و از همهٔ اعلان‌های عادی بالاتر می‌پرد.
  2. style روی خودِ عنصر. اعلانی که در attributeِ style نوشته شده، از هر اعلانی که از راهِ یک selector رسیده قوی‌تر است — هر چقدر هم آن selector سنگین باشد.
  3. specificity. تازه اینجا شمردن شروع می‌شود.
  4. ترتیب در فایل. فقط بینِ قاعده‌هایی که تا اینجا هم‌وزن مانده‌اند: آنکه پایین‌تر نوشته شده می‌برد.

یک پلهٔ دیگر هم هست که فقط اگر @layer بنویسی وجود دارد: ترتیبِ layer ها بینِ پلهٔ ۲ و ۳ می‌نشیند و از specificity قوی‌تر است.

یک مثالِ آشنا از پلهٔ اول: رنگِ پیش‌فرضِ لینک‌ها در مرورگر با یک pseudo-class نوشته شده و specificityاش از a خالی بالاتر است — در پنل Styles زیر user agent stylesheet می‌بینی‌اش. با این حال یک a { color: inherit; } ساده رویش را می‌پوشاند؛ نه به‌خاطر شمردن، بلکه چون استایلِ تو author است.

حالا معروف‌ترین سوءتفاهم. «قاعدهٔ من پایین‌تر است پس برنده است» فقط در پلهٔ ۴ درست است:

#intro {
  color: crimson;
}

p {
  color: gray;
}

روی <p id="intro"> متن قرمز می‌ماند. p آخر نوشته شده و باز هم می‌بازد، چون کارش در پلهٔ ۳ تمام شد و اصلاً به پلهٔ ۴ نرسید.

ترتیب واقعاً کار می‌کند، ولی فقط بینِ هم‌وزن‌ها:

.lead {
  color: teal;
}

.intro {
  color: gray;
}

روی <p class="lead intro"> متن خاکستری می‌شود. و یک نکته که خیلی‌ها را گیر می‌اندازد: ترتیبِ class ها داخلِ HTML هیچ اثری ندارد. بنویسی class="intro lead" باز هم خاکستری است؛ تنها ترتیبی که مرورگر می‌بیند، ترتیبِ قاعده‌ها در فایلِ CSS است.

specificity، درست شمرده

specificity یک عدد نیست؛ سه گروه است که جدا شمرده می‌شوند:

  • گروه اول: چند تا #id.
  • گروه دوم: چند تا class، selectorِ صفتی ([type="email"]) و pseudo-class (:hover).
  • گروه سوم: چند تا اسمِ tag (p) و pseudo-element (::before).

چیزهایی که اصلاً شمرده نمی‌شوند: combinator ها (فاصله، >، +، ~)، selectorِ سراسری *، و هر چیزی که داخلِ :where() باشد.

مقایسه از گروهِ اول شروع می‌شود؛ اگر مساوی بود گروه دوم، و بعد گروه سوم. و اینجا مهم‌ترین جملهٔ این نوشته است: این یک عدد در مبنای ده نیست و هیچ رقمی به رقمِ بعدی سرریز نمی‌کند. ده تا class نمی‌شود یک id؛ یازده تا هم نمی‌شود؛ هزار تا هم نمی‌شود.

selector id class / صفت / pseudo-class tag / pseudo-element وزن
* 0 0 0 0,0,0
p 0 0 1 0,0,1
header nav ul li a 0 0 5 0,0,5
.note 0 1 0 0,1,0
p.note 0 1 1 0,1,1
.card .title 0 2 0 0,2,0
input[type="email"] 0 1 1 0,1,1
.card::before 0 1 1 0,1,1
li:not(.active) 0 1 1 0,1,1
:where(.card) p 0 0 1 0,0,1
:is(.card, #hero) p 1 0 1 1,0,1
#intro 1 0 0 1,0,0
#main .card a 1 1 1 1,1,1

و حالا همین وزن‌ها در برابر هم:

این selector در برابر برنده چرا
.a.b.c.d.e.f.g.h.i.j.k (0,11,0) #price (1,0,0) #price گروهِ اول اول مقایسه می‌شود: 1 در برابر 0. یازده class هیچ‌وقت یک id نمی‌شود.
header nav ul li a (0,0,5) .nav-link (0,1,0) .nav-link گروهِ دوم: 1 در برابر 0. پنج tag یک class نمی‌شود.
#main .card a (1,1,1) #main a (1,0,1) #main .card a گروهِ اول مساوی؛ گروهِ دوم تصمیم می‌گیرد.
p.note (0,1,1) .note (0,1,0) p.note دو گروهِ اول مساوی؛ گروهِ سوم تصمیم می‌گیرد.
.lead (0,1,0) .intro (0,1,0) آنکه پایین‌تر نوشته شده کاملاً هم‌وزن‌اند؛ تنها جایی که ترتیب حرف می‌زند.

سطرِ اول را می‌شود کپی کرد و با چشم دید. این HTML:

<!DOCTYPE html>
<html lang="fa" dir="rtl">
  <head>
    <meta charset="UTF-8">
    <title>یازده class در برابر یک id</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <p id="price" class="a b c d e f g h i j k">قیمت ویژه</p>
  </body>
</html>

و این CSS:

.a.b.c.d.e.f.g.h.i.j.k {
  color: crimson;
}

#price {
  color: navy;
}

متن سرمه‌ای می‌شود. جای دو قاعده را هم عوض کنی باز سرمه‌ای است: ترتیب اینجا هیچ‌کاره است، چون بازی در گروهِ اول تمام شد.

:where()، :is() و :not()

سه pseudo-class مستقیماً روی همین شمارش اثر می‌گذارند و امروز راهِ درستِ کنترلِ وزن‌اند:

  • :where() وزنِ صفر دارد — نه خودش، نه چیزی که داخلش بگذاری. :where(.card) p دقیقاً برابر p است: 0,0,1.
  • :is() وزنِ قوی‌ترین آرگومانش را برمی‌دارد. :is(.card, #hero) p می‌شود 1,0,1 — آن #hero کلِ selector را سنگین کرد، حتی وقتی چیزی که match شده .card بوده.
  • :not() هم وزنِ قوی‌ترین آرگومانش را برمی‌دارد. li:not(.active) می‌شود 0,1,1: یک class از داخلِ :not() به‌علاوهٔ li.

کاربردِ عملی‌اش: قاعده‌های پایه را داخلِ :where() بگذار تا وزنشان صفر بماند و هر قاعدهٔ بعدی، حتی ضعیف‌ترینش، بدون جنگ رویشان بنشیند:

:where(a) {
  color: inherit;
}

.card a {
  color: crimson;
}

inheritance با cascade فرق دارد

خیلی از سردرگمی‌ها از یکی‌گرفتنِ این دو می‌آید. inheritance رقابت نیست؛ چیزی است که وقتی رقابت اصلاً برگزار نشود اتفاق می‌افتد. اگر هیچ اعلانی برای یک property مستقیماً روی عنصر نباشد، بعضی property ها (مثل color و font-family) مقدارشان را از والد می‌گیرند و بعضی دیگر (مثل background-color، border، padding) نمی‌گیرند.

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

<!DOCTYPE html>
<html lang="fa" dir="rtl">
  <head>
    <meta charset="UTF-8">
    <title>ارث در برابر قاعدهٔ مستقیم</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body id="page" class="theme-dark">
    <main class="content">
      <p>این متن رنگش را از بالا می‌گیرد.</p>
      <p>و این یکی یک <a href="/blog/html-vs-css">لینک</a> دارد.</p>
    </main>
  </body>
</html>

و این CSS:

#page.theme-dark .content {
  color: navy;
}

a {
  color: crimson;
}

قاعدهٔ بالا وزنِ 1,2,0 دارد و پایینی 0,0,1 — یعنی تقریباً هیچ. با این حال لینک قرمز می‌شود. چون آن سرمه‌ای هیچ‌وقت برای <a> رقابت نکرد: اعلانی بود روی .content که از آنجا به فرزندانش سرازیر شد. به‌محضِ اینکه یک اعلانِ مستقیم روی خودِ <a> باشد، همان می‌نشیند. specificity اصلاً وارد ماجرا نشد.

همین سؤالِ همیشگی را جواب می‌دهد: چرا body { color: … } را یک a { } خالی خراب می‌کند؟ چون آن رنگ هیچ‌وقت مالِ لینک نبود؛ فقط تا آنجا سُر خورده بود.

در پنل Styles هم این تفاوت را می‌بینی: مقدارهای ارث‌رسیده زیر بخشِ جداگانهٔ Inherited from می‌آیند، نه در فهرستِ قاعده‌های خودِ عنصر. برای کنترلِ صریح، inherit می‌گوید «مقدارِ والدت را بگیر» و initial همان property را به مقدارِ کارخانه‌ای برمی‌گرداند.

!important: کِی واقعاً لازم است

اول ببین !important کجا می‌ایستد. سمتِ تو (author) نردبان چهار پله دارد، از ضعیف به قوی:

  1. اعلانِ عادی در فایلِ CSS
  2. اعلانِ عادی در style روی خودِ عنصر
  3. اعلانِ !important در فایلِ CSS
  4. اعلانِ !important در style روی عنصر

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

از همین نردبان دو کاربردِ درست درمی‌آید:

یک: استایلی که نمی‌توانی ویرایشش کنی. یک widget که کدش دستِ تو نیست، یا اسکریپتی که مستقیم روی عنصر style می‌نویسد. پلهٔ ۲ را با هیچ selectorی نمی‌شود شکست — نه با یک id، نه با ده تا class. تنها راهِ author برای پوشاندنِ یک inline styleِ عادی، پلهٔ ۳ است:

<p class="badge" style="color: gray;">وضعیت</p>
.badge {
  color: crimson !important;
}

بدونِ آن !important، حتی #page .badge.badge (وزن 1,2,0) هم می‌بازد، چون شمارش اصلاً به آن پله نمی‌رسد.

دو: utility ای که کارش دقیقاً بردن است. یک class مثلِ «پنهانش کن» قرار نیست با کسی مذاکره کند:

.is-hidden {
  display: none !important;
}

اینجا !important یک تصمیمِ آگاهانه است روی یک property، نه یک بلوکِ کامل.

هر جای دیگری اشتباه است: !important رقابت را حذف نمی‌کند، فقط یک طبقه بالاتر می‌بردش — جایی که تنها ابزارِ باقی‌مانده‌ات یک !importantِ دیگر است. آن بالا هم همان قانون‌ها برقرارند؛ بینِ اعلان‌های !importantِ هم‌origin باز specificity تصمیم می‌گیرد:

p {
  color: gray !important;
}

.lead {
  color: teal !important;
}

روی <p class="lead"> نتیجه سبزآبی است. همان بازی، همان شمارش، فقط با گزینه‌های کمتر.

یک محکِ ساده: اگر !important می‌نویسی و هر دو فایل مالِ خودت است، یعنی selectorَت غلط انتخاب شده، نه اینکه آن property به زور نیاز دارد.

وقتی specificity مقصر نیست

از بیرون همه یک شکل‌اند: «یک قاعده نوشتم، هیچ اتفاقی نیفتاد». ولی علت‌ها فرق دارند و تشخیصشان ساده است. این سه علامت را در پنل Styles یاد بگیر:

  • قاعده دیده می‌شود ولی خط خورده → به cascade باخته. اینجا شمردن به دردت می‌خورد.
  • قاعده اصلاً دیده نمی‌شود → به هیچ عنصری نخورده. شمردن بی‌فایده است.
  • یک خط کم‌رنگ با علامتِ هشدار → مرورگر خودِ آن اعلان را نفهمیده و دورش انداخته.

حالا چهار علتی که همه شبیهِ باختن به‌نظر می‌رسند:

۱. selector به هیچ عنصری نمی‌خورد. یک حرفِ کم‌وزیاد در اسمِ class یا تفاوتِ حروفِ بزرگ و کوچک کافی است. کلاسیک‌ترینش: .card .title (با فاصله) یعنی «یک .title که داخلِ .card است»، ولی .card.title (بی‌فاصله) یعنی «یک عنصر که هر دو class را با هم دارد». تشخیص: قاعده در پنل Styles برای آن عنصر اصلاً نیست.

۲. اسمِ property غلط است یا مقدار نامعتبر. colour به‌جای color، یا font-weight: heavy، یا یک کدِ رنگِ ناقص. مرورگر همان یک اعلان را موقعِ خواندن دور می‌اندازد و بقیهٔ بلوک سالم می‌ماند. (این با جاافتادنِ ; فرق دارد: آنجا دو خط به هم می‌چسبند و هر دو از بین می‌روند.) تشخیص: آن خط کم‌رنگ است و علامتِ هشدار دارد — و خط نخورده، چون قاعدهٔ دیگری شکستش نداده.

۳. واحد غلط یا جاافتاده. width: 300 نامعتبر است و دور انداخته می‌شود؛ باید 300px باشد. دو استثنا: 0 واحد نمی‌خواهد، و line-height: 1.5 عمداً بی‌واحد نوشته می‌شود. تشخیص: مثلِ علتِ ۲ — یک خطِ کم‌رنگ با هشدار، وسطِ بلوکی که بقیه‌اش کار می‌کند.

۴. یک shorthandِ بعدی، longhandِ تو را صفر کرده. موذی‌ترینشان، چون هر دو خط مالِ خودت و معتبرند:

.card {
  background-color: gold;
  background: url("/images/paper.png") no-repeat center;
}

background یک shorthand است و همهٔ longhand هایش را تعیین می‌کند؛ آن‌هایی که اسمشان را نبردی به مقدارِ اولیه‌شان برمی‌گردند، پس background-color می‌شود شفاف. همین اتفاق با margin-inline-start که بعدش margin: 0 بیاید تکرار می‌شود. تشخیص: longhandِ تو خط خورده و برنده، همان shorthandِ چند سطر پایین‌تر است — در همان قاعده، با همان specificity، فقط دیرتر. راه‌حل: shorthand را اول بنویس و longhandِ استثنا را بعدش.

تا وقتی این چهار تا را رد نکرده‌ای، شمردنِ specificity وقت تلف‌کردن است.

پنج قاعده که کارت را ساده می‌کند

  1. با class استایل بده و id را برای استایل کنار بگذار. یک id وزنِ 1,0,0 دارد و هیچ ترکیبی از class رویش را نمی‌پوشاند؛ هر بار سقفی می‌سازی که فردا باید از زیرش رد شوی.
  2. selector را کوتاه و صاف نگه دار. header nav ul li a هم سنگین است (0,0,5) هم شکننده — کافی است یک <ul> جابه‌جا شود تا از کار بیفتد. .nav-link هم می‌برد (0,1,0)، هم به ساختارِ HTML بند نیست.
  3. پیش‌فرض‌ها را داخلِ :where() بنویس. وزنشان صفر می‌ماند و هر قاعدهٔ بعدی بدون جنگ رویشان می‌نشیند. :is() هم برای گروه‌کردن عالی است، ولی یک #id وسطِ لیست کلِ selector را سنگین می‌کند.
  4. وقتی واقعاً باید ببری، کمترین وزنِ لازم را اضافه کن. تکرارِ همان class — .btn.btn با وزنِ 0,2,0 — از یک #id یا یک !important بهتر است: قابلِ پیش‌بینی است، به HTML دست نمی‌زند، و سقف نمی‌سازد.
  5. اول نگاه کن، بعد حدس بزن. پنل Styles را باز کن و همان سه علامت را ببین: خط‌خورده یعنی به cascade باخته، غایب یعنی اصلاً نخورده، کم‌رنگ با هشدار یعنی نامعتبر.

بعدش چه بخوانم

اگر هنوز چیزی سرِ جایش نمی‌نشیند، مطمئن شو مسئله‌ات از جنسِ ظاهر است و نه ساختار — HTML و CSS چه فرقی دارند؟ همین مرز را می‌کشد، و چرا فقط div کافی نیست؟ نشان می‌دهد چطور HTMLِ معنادار نصفِ قاعده‌هایت را ساده می‌کند. اگر هم مشکل «جعبه جای اشتباهی است» باشد، آن اصلاً بحثِ cascade نیست؛ flexbox یا grid؟ نقطهٔ شروعِ درست است.

و اگر می‌خواهی این‌ها را قدم‌به‌قدم و با آزمایشگاهی که همان لحظه نتیجه را نشانت می‌دهد یاد بگیری، خودآموزِ تاروپود از همین‌جا شروع می‌کند: شمردن به‌جای حدس‌زدن.