עיצוב מבוסס-תוכן

תכונות אינטרנט: ‎:has()‎ ‎:not()‎

בעבר, כדי להחיל פריסות שונות על רכיב על סמך התוכן שלו, היה צריך להשתמש ב-JavaScript או בלוגיקה מותנית בשפת התבניות של ה-HTML כדי להוסיף מחלקות של משתנים (כמו .card--has-image או .card--text-only).

הפסאודו-מחלקה :has() מייתרת את הצורך הזה כי היא פועלת כבורר הורה. הוא מאפשר לעצב רכיב מאגר לפי תנאי, על סמך הנוכחות או ההיעדרות של רכיבים צאצאים ספציפיים.

באמצעות :has(), אתם יכולים להגדיר בקלות וריאציות פריסה שונות ב-CSS, על סמך תוכן ה-DOM בפועל של הרכיב. אפשר גם לשלב אותו עם :not() כדי לטרגט באופן מפורש את היעדר התוכן ולהגדיר פריסות ברירת מחדל.

הטמעת סגנון קונטיינר שמבוסס על תוכן

חובה: צריך להשתמש בסלקטור :has() ברכיב הקונטיינר כדי לזהות נוכחות של תוכן צאצא ספציפי.

כדי ליצור רכיב שמשנה את הפריסה שלו בהתאם לתוכן:

  1. הגדרת סגנון ברירת המחדל: מחילים את סגנונות הפריסה הבסיסיים על רכיב הקונטיינר (למשל, ערימה פשוטה של עמודה אחת).
  2. החלת שינויים שמבוססים על תוכן: מטרגטים את מאגר התגים באמצעות :has([child-selector]) ומחילים את סגנונות הפריסה החדשים כשהתוכן הזה קיים (למשל, רשת עם כמה עמודות).

דוגמה: רכיב כרטיס שעובר לפריסה של זה לצד זה אם יש תמונה.

/* 1. Define the default state on the component container */
/* This applies when there is NO image */
.article-card {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1.5rem;
  border: 1px solid #ccc;
  border-radius: 8px;
}

/* 2. Apply styles when a specific child element is present */
/* MANDATORY: Target the container and use :has() to check for the descendant */
.article-card:has(img) {
  /* Change the layout to a row if an image exists */
  flex-direction: row;
  align-items: center;
}

/* Optional: Style the image itself (no :has() needed here) */
.article-card img {
  width: 150px;
  height: auto;
  border-radius: 4px;
}

/* You can also combine :has() and :not() to explicitly target the ABSENCE of content */
/* This selects a card that DOES NOT have an image */
.article-card:not(:has(img)) {
  /* E.g., apply a different background color for text-only cards */
  background-color: #f9f9f9;
}
<!-- Assume an <h1> precedes these components in the document layout -->
<!-- A card WITH an image (will use row layout) -->
<article class="article-card">
  <img src="thumbnail.jpg" alt="Article thumbnail" />
  <div class="content">
    <h2>Card With Image</h2>
    <p>This card lays out its content horizontally.</p>
  </div>
</article>

<!-- A card WITHOUT an image (will use default column layout) -->
<article class="article-card">
  <div class="content">
    <h2>Text-Only Card</h2>
    <p>This card lays out its content vertically, and gets its background color from the :not(:has()) rule.</p>
  </div>
</article>

טיפ לשיפור הביצועים: כשמשתמשים ב-:has(), כדאי להגדיר את היקף הסלקטור למאגר הרכיבים הספציפי ביותר שאפשר (כמו .article-card). אם השינויים בסגנון הם מקומיים מאוד, כדאי להימנע מעיגון שלו לאלמנטים ברמה גבוהה מאוד כמו body:has(img), כי שאילתות :has() רחבות יכולות להפעיל יותר חישובים מחדש של הפריסה.

אסטרטגיות חלופיות

אם סגנון הפריסה שמבוסס על תוכן הוא קריטי לחוויית המשתמש או לעיצוב הדף, צריך לספק חלופה לדפדפנים שלא תומכים בסלקטור :has(). לאפקטים דקורטיביים בלבד, אפשר להשתמש ב-:has() כשיפור הדרגתי (progressive enhancement) בלי חלופה.

חובה: כשמטמיעים חלופה לפריסות קריטיות, צריך להשתמש ב-@supports not selector(:has(*)) ב-CSS כדי להגדיר חלופה מסורתית שמבוססת על מחלקות (למשל, .has-image).

בניגוד לעיצוב אינטראקטיבי שמבוסס על מצב, נוכחות התוכן ידועה בדרך כלל בזמן הרינדור. הפתרון החלופי הכי חזק הוא להגדיר שמנוע התבניות בצד השרת (או מחולל האתרים הסטטיים) יוסיף למאגר מחלקה כמו .has-image אם רכיב הצאצא (כמו תמונה) קיים בנתונים.

אם אי אפשר להשתמש בעיבוד בצד השרת, צריך להשתמש בסקריפט קטן עם CSS.supports() כדי לזהות את התוכן ולהוסיף את המחלקה בזמן הטעינה או אחרי הוספת תוכן דינמי.

/* Fallback CSS for older browsers */
/* We check if the browser DOES NOT support the :has() selector */
@supports not selector(:has(*)) {
  /* Define a traditional modifier class that applies the exact same layout overrides */
  .article-card.has-image {
    flex-direction: row;
    align-items: center;
  }
  
  .article-card:not(.has-image) {
    background-color: #f9f9f9;
  }
}
/* Fallback JavaScript for older browsers (if not using SSR to add the class) */
/* Check for support before running the script to avoid unnecessary work in modern browsers */
if (!CSS.supports('selector(:has(*))')) {
  // Find all components that need checking
  const cards = document.querySelectorAll('.article-card');
  
  cards.forEach(card => {
    // If the critical content exists, manually add the fallback class
    if (card.querySelector('img')) {
      card.classList.add('has-image');
    }
  });
}