การจัดรูปแบบตามสถานะย่อย

ฟีเจอร์บนเว็บ: :has() :not()

ในอดีต ตัวเลือก CSS สามารถข้ามลงด้านล่างได้เท่านั้น คุณสามารถจัดสไตล์องค์ประกอบย่อยตามองค์ประกอบหลักได้ แต่ไม่สามารถจัดสไตล์องค์ประกอบหลักตามองค์ประกอบย่อยได้ คลาสเทียม :has() จะเปลี่ยนลักษณะนี้ โดยช่วยให้คุณจัดรูปแบบองค์ประกอบคอนเทนเนอร์ตามเงื่อนไขได้ ทั้งนี้ขึ้นอยู่กับการมีอยู่หรือสถานะขององค์ประกอบสืบทอด

การรวม :has() เข้ากับคลาสเทียมของสถานะ (เช่น :checked, :focus, :valid, :invalid หรือ :not()) ช่วยให้คุณสร้างคอมโพเนนต์ UI แบบอินเทอร์แอกทีฟที่ซับซ้อนได้ทั้งหมดใน CSS โดยไม่ต้องใช้ JavaScript เพื่อสลับคลาส "ตัวแก้ไข" (เช่น .is-active หรือ .has-error) ในองค์ประกอบหลัก

ซึ่งมีประโยชน์อย่างยิ่งสำหรับคอมโพเนนต์ที่ต้องตอบสนองต่อการโต้ตอบภายใน เช่น ปุ่มเปิด/ปิดธีมที่แปลแล้วซึ่งตอบสนองต่อช่องทำเครื่องหมาย (:checked) กลุ่มแบบฟอร์มที่ไฮไลต์ข้อผิดพลาด (:invalid) หรือการ์ดที่ยกระดับเมื่อโฟกัสลิงก์ย่อย (:focus-within)

การใช้การจัดรูปแบบคอนเทนเนอร์ตามสถานะ

บังคับ: คุณต้องใช้ตัวเลือก :has() ในองค์ประกอบคอนเทนเนอร์เพื่อตรวจจับสถานะที่เฉพาะเจาะจง (เช่น :checked, :focus, :invalid) ขององค์ประกอบย่อยแบบอินเทอร์แอกทีฟ

วิธีสร้างคอมโพเนนต์ที่เปลี่ยนการจัดสไตล์ตามสถานะขององค์ประกอบย่อย

  1. กำหนดการจัดรูปแบบเริ่มต้น: ตั้งค่าตัวแปร CSS ในคอนเทนเนอร์เพื่อกำหนดสถานะพื้นฐาน
  2. ใช้การลบล้างตามสถานะ: กำหนดเป้าหมายคอนเทนเนอร์ด้วย :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() เป็นการเพิ่มประสิทธิภาพแบบต่อเนื่องโดยไม่มีการสำรอง

บังคับ: เมื่อใช้ฟีเจอร์สำรองสำหรับฟีเจอร์ที่สำคัญ คุณต้องใช้ @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);
    });
  }
}