تاروپود — خودآموز HTML و CSS

فصل ۹ از ۱۰

پیشرفت ترم
۰٪

ترم ۵ · تعامل و جزئیات

:has() و تعامل بدون JavaScript

فصل ۹پیش‌نمایش رایگان

در این فصل چه می‌سازی#

تا حالا هر selector یی که نوشتی رو به پایین بود: یک عنصر را انتخاب می‌کردی، یا فرزندش را، یا برادرِ بعدی‌اش. هیچ‌وقت نتوانستی بگویی «این عنصر را انتخاب کن، چون فرزندی با فلان حالت دارد». در این فصل یک selector تازه می‌بینی که همین را ممکن می‌کند: :has(). با آن یک کارت می‌تواند وقتی checkbox داخلش تیک بخورد، خودش عوض شود — و این یعنی تعاملِ واقعی، بدونِ حتی یک خط JavaScript. آخر فصل کارتی داری که با یک تیک، از روشن به تیره می‌رود.

یک کارت که یک checkbox داخلش دارد؛ از خودِ کارت یک فلش رو به بالا به کلِ کارت برمی‌گردد و نشان می‌دهد که کارت بر اساسِ وضعیتِ فرزندش رنگ عوض می‌کند

آخر این فصل می‌توانی:

  • بگویی چرا با selector معمولی نمی‌شود یک عنصر را بر اساسِ فرزندش استایل داد
  • با :has() یک عنصرِ والد را بر اساسِ چیزی که درونش است انتخاب کنی
  • :has() را با :checked و :invalid ترکیب کنی تا صفحه بدونِ JavaScript واکنش نشان دهد
  • با :target بر اساسِ آدرسِ صفحه یک بخش را نشان یا پنهان کنی

قبل از شروع#

  • از فصل ۵ (همین ترم): :checked و :invalid — حالتِ فرم که :has() روی آن‌ها می‌نشیند.
  • از فصل ۷ (همین ترم): transition — تا تغییرِ کارت نرم باشد نه ناگهانی.
  • از فصل ۴ (همین ترم): accent-color — رنگِ خودِ checkbox.

مثل همیشه فقط یک مرورگر لازم داری، یا فقط همین آزمایشگاه.

۱. آرزوی ناممکن: یک والد که به فرزندش واکنش می‌دهد#

می‌خواهیم وقتی checkbox داخلِ کارت تیک می‌خورد، کلِ کارت تیره شود. اولین فکری که به ذهنت می‌رسد این است: «کارت را بگیر، checkbox تیک‌خورده‌اش را پیدا کن، رنگ را عوض کن». پس این را می‌نویسی:

.card input:checked {
  background-color: #1e2530;
}

در آزمایشگاه امتحانش کن و تیک را بزن. کارت هیچ تغییری نمی‌کند. نهایتاً خودِ کادرِ کوچکِ checkbox کمی فرق می‌کند، ولی کارت همان روشنِ قبلی می‌ماند.

🔧 اگر کار نکرد: این خطا نیست، این یک محدودیتِ واقعیِ selector هاست. .card input:checked یعنی «آن checkbox تیک‌خورده‌ای که داخلِ کارت است» — تو داری خودِ checkbox را انتخاب می‌کنی، نه کارت را. همهٔ selector هایی که تا حالا دیدی رو به پایین کار می‌کنند: از والد به فرزند. هیچ combinator ای وجود ندارد که «برو بالا به والد». برای همین این کار سال‌ها فقط با JavaScript ممکن بود — تا اینکه :has() آمد.

۲. :has(): selectorِ والد#

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

.card:has(input:checked) {
  background-color: #1e2530;
  color: #f2f4f8;
  border-color: #2c3644;
}

این را بخوان: «کارتی را انتخاب کن که یک input:checked دارد». حالا انتخاب‌شده خودِ کارت است، نه checkbox. این را جای‌گزینِ قاعدهٔ بخش قبل کن و در آزمایشگاه تیک را بزن.

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

💡 نکته: اسمِ :has() را «والدِ» ترجمه نکن؛ دقیق‌تر این است: «عنصری که چیزی را دارد». می‌تواند بر اساسِ فرزند کار کند، ولی همچنین بر اساسِ برادرِ بعدی — مثلاً label:has(+ input:invalid). ولی مدلِ ذهنیِ ساده‌ات همان «انتخابِ والد بر اساسِ فرزند» است و ۹۰٪ کارها همین است.

۳. :has() فقط برای checkbox نیست#

قدرتِ واقعیِ :has() این است که با هر selector دیگری ترکیب می‌شود. یک نمونهٔ خیلی کاربردی: یک جعبهٔ فرم که وقتی ورودیِ نامعتبرِ داخلش خطا دارد (همان :invalid فصل ۵)، خودش قرمز شود تا کاربر جای اشکال را ببیند:

.field:has(input:invalid) {
  border-color: #bc3e1c;
}

اینجا هم والد (.field) دارد بر اساسِ حالتِ فرزندش (input:invalid) استایل می‌گیرد — بی‌هیچ JavaScript. قبلاً برای این کار باید با کد به هر ورودی گوش می‌دادی؛ حالا یک خط CSS است.

💡 نکته: :has() امروز در همهٔ مرورگرهای اصلی کار می‌کند و می‌توانی بی‌نگرانی به‌کارش ببری. اگر خواستی خیلی محتاط باشی، می‌توانی یک قاعدهٔ مهم را داخلِ @supports selector(:has(*)) بگذاری تا اگر کسی با مرورگرِ خیلی قدیمی آمد، صفحه دستِ‌کم نشکند — ولی برای بیشترِ کارها لازم نیست.

:has() جای خوبی است که AI را هم امتحان کنی، چون جوابش گاهی از دنیای دیروز است:

🤖 با AI: از دستیار AI ات بپرس «در CSS چطور یک عنصرِ والد را بر اساسِ حالتِ فرزندش استایل بدهم؟». جالب است که خیلی وقت‌ها اول سراغِ JavaScript می‌رود، چون سال‌ها جوابِ درست همان بود. اگر این‌طور شد، خودت بپرس «با :has() بدونِ JavaScript می‌شود؟» و جوابش را در آزمایشگاه چک کن — این خودش درسِ خوبی است که دانشِ AI گاهی از دنیای دیروز است.

۴. :target: واکنش به آدرسِ صفحه#

یک راهِ دومِ تعاملِ بدونِ JavaScript هم داریم که به‌کار می‌آید: :target. یادت هست از ترم ۱ که لینک می‌تواند به یک #id داخلِ صفحه بپرد؟ وقتی این اتفاق می‌افتد، آدرسِ صفحه آن #id را می‌گیرد، و عنصری که آن id را دارد می‌شود «هدف». :target دقیقاً همان عنصرِ هدف را انتخاب می‌کند.

با این می‌شود چیزی را پنهان نگه داشت تا وقتی کاربر رویش لینک بزند:

<p><a href="#more">بیشتر بخوان</a></p>
<p id="more" class="more">این توضیحِ بیشتر تا وقتی روی لینک نزده‌ای پنهان است.</p>
.more {
  display: none;
}

.more:target {
  display: block;
}

روی «بیشتر بخوان» کلیک کن: آدرس به #more می‌رود، آن پاراگراف می‌شود :target و ظاهر می‌شود — بی‌هیچ JavaScript. :checked حالت را در خودِ فرم نگه می‌دارد، :target حالت را در آدرسِ صفحه؛ هر دو ابزارِ تعاملِ بی‌جاوااسکریپت‌اند، و در فصل بعد در کنارِ هم یک component کامل می‌سازیم.

۵. کد کاملِ این فصل#

اول index.html:

<!-- index.html -->
<!DOCTYPE html>
<html lang="fa" dir="rtl">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>:has()</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <article class="card">
      <h2>کارتِ من</h2>
      <p>این کارت وقتی تیک را بزنی، خودش عوض می‌شود — بدون یک خط JavaScript.</p>
      <label class="toggle">
        <input type="checkbox">
        حالتِ تیره
      </label>
    </article>
  </body>
</html>

و styles.css:

/* styles.css */
* {
  box-sizing: border-box;
}

:root {
  --bg: #f6edda;
  --card: #ffffff;
  --ink: #2c2114;
  --brand: #2a6b93;
  --line: #d8ccae;
}

body {
  margin: 0;
  font-family: Tahoma, sans-serif;
  background-color: var(--bg);
  color: var(--ink);
  padding: 40px;
  line-height: 1.9;
}

.card {
  max-width: 360px;
  margin-inline: auto;
  padding: 24px;
  background-color: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 16px;
  transition: background-color 0.3s, color 0.3s, border-color 0.3s;
}

.card h2 {
  margin-block: 0 8px;
}

.toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-block-start: 16px;
  cursor: pointer;
  accent-color: var(--brand);
}

/* :has() -> vaghti card yek checkbox-e checked dashte bashe, khod-e card avaz mishe */
.card:has(input:checked) {
  background-color: #1e2530;
  color: #f2f4f8;
  border-color: #2c3644;
}

دو حالتِ یک کارت: چپ روشن با checkbox خالی، راست تیره با checkbox تیک‌خورده؛ فلشی از تیک به کلِ کارت نشان می‌دهد که تیک، کارت را تیره کرده

چک کن: کارت با تیک تیره و بی‌تیک روشن می‌شود، و بینشان نرم جابه‌جا می‌شود. حالا یک آزمایشِ کوچک: قاعدهٔ .card:has(input:checked) را به .card:has(input) عوض کن (بدونِ :checked). حالا کارت همیشه تیره است، چون همیشه یک input دارد — این نشان می‌دهد که شرطِ داخلِ پرانتز دقیقاً چه‌کار می‌کند. برگردانش.

واژه‌های تازهٔ این فصل#

کلمه تلفظ به حروف فارسی یعنی چه
:has() هَز selector ای که یک عنصر را انتخاب می‌کند بر اساسِ آنچه درونش دارد
parent selector پِرِنت سِلِکتور نامِ خودمانیِ :has()؛ انتخابِ والد بر اساسِ فرزند
:target تارگِت عنصری که id اش با # در آدرسِ صفحه یکی است

تمرین‌ها

اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.

در فصل بعد#

الان دو ابزارِ تعاملِ بدونِ JavaScript داری — :has() با :checked، و :target — و کلِ ترم ۵ را هم پشتِ سر گذاشته‌ای: حالت‌ها، combinator ها، pseudo-element ها، فرم و اعتبارسنجی، transform، transition و animation. فصل بعد فصلِ پروژه است: مفهومِ تازه‌ای ندارد و همهٔ این‌ها را در یک component تعاملیِ واقعی — یک آکاردئون که با صفحه‌کلید هم کار می‌کند — کنارِ هم می‌گذاری، همه بدونِ یک خط JavaScript.

به آخر این فصل رسیدی!

اگر ساختی و جواب داد، این دکمه مال توست.