בעבר, סלקטורים ב-CSS יכלו לעבור רק כלפי מטה – אפשר היה לעצב רכיב צאצא על סמך רכיב ההורה שלו, אבל לא רכיב הורה על סמך רכיב הצאצא שלו. הפסאודו-מחלקה :has() משנה את זה, ומאפשרת לעצב באופן מותנה רכיב מאגר בהתאם לנוכחות או למצב של הצאצאים שלו.
כשמשלבים את :has() עם מחלקות פסאודו של מצב (כמו :checked, :focus, :valid, :invalid או :not()), אפשר ליצור רכיבי ממשק משתמש מורכבים ואינטראקטיביים ב-CSS בלבד, בלי צורך ב-JavaScript כדי להחליף מחלקות "משנות" (כמו .is-active או .has-error) ברכיבי הורה.
זה שימושי במיוחד לרכיבים שצריכים להגיב לאינטראקציות פנימיות, כמו מתג עיצוב מקומי שמגיב לתיבת סימון (:checked), קבוצת טפסים שמדגישה שגיאה (:invalid) או כרטיס שמוגבה כשמתמקדים בקישור צאצא (:focus-within).
הטמעת עיצוב קונטיינר מבוסס-מצב
חובה: צריך להשתמש בבורר :has() ברכיב מאגר התגים כדי לזהות את המצב הספציפי (למשל, :checked, :focus, :invalid) של רכיב הצאצא האינטראקטיבי שלו.
כדי ליצור רכיב שמשנה את הסגנון שלו בהתאם למצב של רכיב צאצא:
- הגדרת סגנון ברירת המחדל: מגדירים משתני CSS בקונטיינר כדי להגדיר את מצב הבסיס שלו.
- החלת שינויים שמבוססים על מצב: מטרגטים את מאגר התגים באמצעות
:has([child-selector]:[state])ומגדירים מחדש את משתני ה-CSS למצב הפעיל או החלופי.
דוגמה: רכיב שמשנה את העיצוב על סמך מתג משני.
/* 1. Define the default state on the component container */
.theme-card {
/* Using custom properties makes state-switching cleaner */
--card-bg: #ffffff;
--card-text: #333333;
--card-border: #cccccc;
background-color: var(--card-bg);
color: var(--card-text);
border: 1px solid var(--card-border);
/* Use a transition for smooth state changes */
transition: background-color 0.3s, color 0.3s;
}
/* 2. Apply styles when the child enters the specific state */
/* MANDATORY: Target the container and use :has() to check the descendant's state */
.theme-card:has(.theme-toggle:checked) {
/* Override the properties for the active or alternate state */
--card-bg: #222222;
--card-text: #f0f0f0;
--card-border: #555555;
}
/* You can also combine :has() and :not() to target specific negative states */
/* This selects a card that DOES NOT have a toggle in the checked state */
.theme-card:not(:has(.theme-toggle:checked)) {
/* Optional: Explicit styles for the unchecked state if needed */
}
<!-- The container element that receives the styling -->
<div class="theme-card">
<!-- The child element whose state controls the parent -->
<label>
<input type="checkbox" class="theme-toggle">
Enable Dark Mode
</label>
<h2>Card Title</h2>
<p>The style of this entire card is controlled by the checkbox above.</p>
</div>
טיפ לשיפור הביצועים: כשמשתמשים ב-:has(), כדאי להגדיר את היקף הבורר למאגר הספציפי ביותר שאפשר (כמו .theme-card). אם השינויים בסגנון הם מקומיים, כדאי להימנע מעיגון שלו לרכיבים ברמה גבוהה מאוד כמו body:has(...), כי שאילתות :has() רחבות יכולות להפעיל יותר חישובים מחדש של הפריסה.
אסטרטגיות חלופיות
אם העיצוב שמבוסס על מצב הוא קריטי לחוויית המשתמש או לפריסת הדף, צריך לספק חלופה לדפדפנים שלא תומכים בבורר :has(). לאפקטים דקורטיביים בלבד, אפשר להשתמש ב-:has() כשיפור הדרגתי (progressive enhancement) בלי חלופה.
חובה: כשמטמיעים חלופה לתכונות קריטיות, צריך להשתמש ב-@supports not selector(:has(*)) ב-CSS כדי להגדיר חלופה מסורתית שמבוססת על מחלקות. אם השינוי במצב הקריטי תלוי באינטראקציה של המשתמש, צריך גם להשתמש בסקריפט קטן מוטבע עם CSS.supports() כדי להחליף את המחלקה הזו על סמך אירוע ה-JavaScript המקביל (למשל, change, focus, blur) שמייצג את השינוי במצב.
/* 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 overrides */
.theme-card.is-active {
--card-bg: #222222;
--card-text: #f0f0f0;
--card-border: #555555;
}
}
/* Fallback JavaScript for older browsers */
/* Check for support before running the script to avoid unnecessary work in modern browsers */
if (!CSS.supports('selector(:has(*))')) {
const toggle = document.querySelector('.theme-toggle');
const card = document.querySelector('.theme-card');
if (toggle && card) {
// Manually toggle the fallback class when the input state changes
toggle.addEventListener('change', (e) => {
card.classList.toggle('is-active', e.target.checked);
});
}
}