:has(): ตัวเลือกครอบครัว

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

คลาสจำลอง CSS :has() จะแสดงองค์ประกอบหากตัวเลือกใดก็ตามที่ส่งเป็นพารามิเตอร์ตรงกับองค์ประกอบอย่างน้อย 1 รายการ

แต่ตัวเลือกนี้ไม่ได้มีไว้สำหรับเลือก "รายการหลัก" เท่านั้น นี่เป็นวิธีที่ดีในการโปรโมต วิธีที่ไม่น่าสนใจมากนักอาจเป็นตัวเลือก "สภาพแวดล้อมแบบมีเงื่อนไข" แต่ชื่อนี้ฟังดูไม่ค่อยเพราะเท่าไหร่ แล้วตัวเลือก "ครอบครัว" ล่ะ

การรองรับเบราว์เซอร์

ก่อนไปต่อ เราขอพูดถึงการรองรับเบราว์เซอร์สักหน่อย ยังไม่พร้อมใช้งาน แต่เรากำลังใกล้ถึงจุดนั้น ยังไม่รองรับ Firefox แต่อยู่ในแผนงาน แต่ฟีเจอร์นี้อยู่ใน Safari อยู่แล้วและพร้อมที่จะเปิดตัวใน Chromium 105 การแสดงตัวอย่างทั้งหมดในบทความนี้จะแจ้งให้คุณทราบหากเบราว์เซอร์ที่ใช้ไม่รองรับ

วิธีใช้ :has

หน้าตาเป็นอย่างไร ลองดู HTML ต่อไปนี้ซึ่งมีองค์ประกอบพี่น้อง 2 รายการที่มีคลาส everybody คุณจะเลือกรายการที่มีรายการที่สืบทอดซึ่งมีคลาส a-good-time ได้อย่างไร

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

เมื่อใช้ :has() คุณจะดำเนินการดังกล่าวได้ด้วย CSS ต่อไปนี้

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

ซึ่งจะเลือกอินสแตนซ์แรกของ .everybody และใช้ animation

ในตัวอย่างนี้ องค์ประกอบที่มีคลาส everybody คือเป้าหมาย เงื่อนไขคือมีรายการที่สืบทอดซึ่งมีคลาส a-good-time

<target>:has(<condition>) { <styles> }

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

เลือกองค์ประกอบ figure ที่มี figcaption โดยตรง css figure:has(> figcaption) { ... } เลือก anchor ที่ไม่มี SVG ที่เป็นลําดับถัดไปโดยตรง css a:not(:has(> svg)) { ... } เลือก label ที่มี input ที่เป็นพี่น้องโดยตรง กำลังไปด้านข้าง css label:has(+ input) { … } เลือก article ที่มี img ซึ่งเป็นรายการที่สืบทอดมาไม่มีข้อความ alt css article:has(img:not([alt])) { … } เลือก documentElement ที่มีสถานะบางอย่างใน DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } เลือกคอนเทนเนอร์เลย์เอาต์ที่มีรายการย่อยเป็นจํานวนคี่ css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } เลือกรายการทั้งหมดในตารางกริดที่ไม่ได้วางเมาส์เหนือไว้ css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } เลือกคอนเทนเนอร์ที่มีองค์ประกอบที่กําหนดเอง <todo-list> css main:has(todo-list) { ... } เลือก a เดี่ยวทุกรายการภายในย่อหน้าที่มีองค์ประกอบ hr ที่เป็นพี่น้องกันโดยตรง css p:has(+ hr) a:only-child { … } เลือก article ที่เป็นไปตามเงื่อนไขหลายข้อ css article:has(>h1):has(>h2) { … } ผสมผสานกัน เลือก article ที่มีชื่อตามด้วยคำบรรยาย css article:has(> h1 + h2) { … } เลือก :root เมื่อมีการเรียกใช้สถานะแบบอินเทอร์แอกทีฟ css :root:has(a:hover) { … } เลือกย่อหน้าที่อยู่หลัง figure ที่ไม่มี figcaption css figure:not(:has(figcaption)) + p { … }

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

ตัวอย่าง

มาดูตัวอย่างการใช้งานกัน

การ์ด

ดูการสาธิตการ์ดแบบคลาสสิก เราอาจแสดงข้อมูลใดก็ได้ในการ์ด เช่น ชื่อ คำบรรยาย หรือสื่อบางอย่าง นี่คือการ์ดพื้นฐาน

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

จะเกิดอะไรขึ้นเมื่อคุณต้องการแสดงสื่อ สำหรับการออกแบบนี้ การ์ดอาจแบ่งออกเป็น 2 คอลัมน์ ก่อนหน้านี้ คุณอาจสร้างคลาสใหม่เพื่อแสดงลักษณะการทำงานนี้ เช่น card--with-media หรือ card--two-columns ชื่อคลาสเหล่านี้ไม่เพียงแต่จะนึกขึ้นมาได้ยากเท่านั้น แต่ยังทำให้ดูแลรักษาและจดจำได้ยากด้วย

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

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

และคุณไม่จำเป็นต้องเก็บไว้ คุณสามารถใช้ไอเดียนี้อย่างสร้างสรรค์ได้ การ์ดที่แสดงเนื้อหา "แนะนำ" อาจปรับให้เข้ากับเลย์เอาต์ได้อย่างไร CSS นี้จะทําให้การ์ดแนะนํากว้างเต็มเลย์เอาต์และวางไว้ที่จุดเริ่มต้นของตารางกริด

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

จะเกิดอะไรขึ้นหากการ์ดแนะนำที่มีแบนเนอร์เคลื่อนไหวเพื่อดึงดูดความสนใจ

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>

.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

โอกาสมากมาย

ฟอร์ม

แล้วแบบฟอร์มล่ะ เพราะเป็นทรงผมที่แต่งทรงยาก ตัวอย่างหนึ่งคือการจัดรูปแบบอินพุตและป้ายกำกับของอินพุต เราจะส่งสัญญาณว่าช่องหนึ่งๆ ถูกต้องได้อย่างไร :has() จะช่วยให้การดำเนินการนี้ง่ายขึ้นมาก เราสามารถใช้กับรูปแบบของคลาสจำลองที่เกี่ยวข้อง เช่น :valid และ :invalid

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

ลองดูตัวอย่างนี้: ลองป้อนค่าที่ถูกต้องและไม่ถูกต้อง รวมถึงเปิดและปิดโฟกัส

นอกจากนี้ คุณยังใช้ :has() เพื่อแสดงและซ่อนข้อความแสดงข้อผิดพลาดของช่องได้ด้วย นำกลุ่มช่อง "email" ของเราไปเพิ่มข้อความแสดงข้อผิดพลาด

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

โดยค่าเริ่มต้น ระบบจะซ่อนข้อความแสดงข้อผิดพลาด

.form-group__error {
  display: none;
}

แต่เมื่อช่องเปลี่ยนเป็น :invalid และไม่ได้โฟกัส คุณจะแสดงข้อความได้โดยไม่ต้องใช้ชื่อคลาสเพิ่มเติม

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

คุณเพิ่มลูกเล่นเล็กๆ น้อยๆ ที่น่าสนใจให้กับแบบฟอร์มได้ ลองดูตัวอย่างนี้ ดูเมื่อคุณป้อนค่าที่ถูกต้องสําหรับการโต้ตอบแบบไมโคร ค่า :invalid จะทำให้กลุ่มแบบฟอร์มสั่น แต่เฉพาะในกรณีที่ผู้ใช้ไม่มีค่ากำหนดการเคลื่อนไหว

เนื้อหา

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

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

ตัวอย่างนี้มีตัวเลข เมื่อไม่มี figcaption ข้อความจะลอยอยู่ในเนื้อหา เมื่อมี figcaption อยู่ figcaption จะใช้พื้นที่เต็มความกว้างและรับระยะขอบเพิ่มเติม

การตอบสนองต่อสถานะ

คุณจะทําให้สไตล์ของคุณตอบสนองต่อสถานะบางอย่างในมาร์กอัปของเราได้ไหม ลองดูตัวอย่างที่มีแถบนําทางแบบเลื่อน "คลาสสิก" หากคุณมีปุ่มสลับการเปิดการนําทาง ปุ่มนั้นอาจใช้แอตทริบิวต์ aria-expanded คุณสามารถใช้ JavaScript เพื่ออัปเดตแอตทริบิวต์ที่เหมาะสมได้ เมื่อ aria-expanded เป็น true ให้ใช้ :has() เพื่อตรวจหาและอัปเดตสไตล์ของการนำทางแบบเลื่อน JavaScript จะทําหน้าที่ของตนและ CSS จะทำสิ่งใดก็ได้กับข้อมูลนั้น คุณไม่จำเป็นต้องย้ายมาร์กอัปไปมาหรือเพิ่มชื่อคลาสเพิ่มเติม เป็นต้น (หมายเหตุ: ตัวอย่างนี้ไม่ใช่ตัวอย่างที่พร้อมใช้งานจริง)

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}

:has ช่วยหลีกเลี่ยงข้อผิดพลาดของผู้ใช้ได้ไหม

ตัวอย่างทั้งหมดนี้มีอะไรเหมือนกัน นอกเหนือจากการแสดงวิธีใช้ :has() แล้ว ก็ไม่จําเป็นต้องแก้ไขชื่อคลาส โดยแต่ละคนได้แทรกเนื้อหาใหม่และอัปเดตแอตทริบิวต์ นี่เป็นข้อดีอย่างหนึ่งของ :has() เนื่องจากช่วยบรรเทาข้อผิดพลาดของผู้ใช้ได้ :has() ช่วยให้ CSS รับผิดชอบในการปรับให้เข้ากับการเปลี่ยนแปลงใน DOM ได้ คุณไม่จําเป็นต้องจัดการชื่อคลาสใน JavaScript ซึ่งจะช่วยลดโอกาสที่จะเกิดข้อผิดพลาดของนักพัฒนาซอฟต์แวร์ เราทุกคนเคยพิมพ์ชื่อชั้นเรียนผิดและต้องเก็บชื่อนั้นไว้ในObjectการค้นหา

แนวคิดนี้น่าสนใจ แต่จะช่วยให้เรามีมาร์กอัปที่สะอาดขึ้นและมีโค้ดน้อยลงไหม JavaScript น้อยลงเนื่องจากเราปรับ JavaScript น้อยลง HTML น้อยลงเนื่องจากคุณไม่จำเป็นต้องใช้คลาสอย่าง card card--has-media ฯลฯ อีกต่อไป

การคิดนอกกรอบ

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

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

ซึ่งเปิดโอกาสที่น่าสนใจ คุณสามารถใช้ข้อมูลนี้เพื่อไปยังส่วนต่างๆ ของแบบฟอร์มด้วยการเปลี่ยนรูปแบบ โปรดทราบว่าการสาธิตนี้เหมาะที่สุดสำหรับการดูในแท็บเบราว์เซอร์แยกต่างหาก

หรือจะลองเล่นเกมคลาสสิกอย่างเส้นลวดไฟฟ้ากัน :has() ช่วยให้สร้างกลไกได้ง่ายขึ้น หากวางเมาส์เหนือสายไฟ เกมจะจบลง ได้ เราสามารถสร้างกลไกเกมเหล่านี้บางส่วนได้ด้วยสิ่งต่างๆ เช่น คอมบิเนเตอร์ (+ และ ~) ที่เป็นพี่น้องกัน แต่ :has() เป็นวิธีที่จะให้ผลลัพธ์เดียวกันโดยไม่ต้องใช้ "เคล็ดลับ" ที่น่าสนใจของมาร์กอัป โปรดทราบว่าการสาธิตนี้เหมาะที่สุดสำหรับการดูในแท็บเบราว์เซอร์แยกต่างหาก

แม้ว่าคุณจะไม่ได้นำข้อมูลเหล่านี้ไปใช้กับเวอร์ชันที่ใช้งานจริงในเร็วๆ นี้ แต่ข้อมูลเหล่านี้จะไฮไลต์วิธีใช้องค์ประกอบพื้นฐาน เช่น ความสามารถในการใช้ :has() แบบเชน

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

ประสิทธิภาพและข้อจํากัด

ก่อนจากกัน โปรดบอกสิ่งที่คุณทำไม่ได้กับ :has() :has() มีข้อจํากัดบางอย่าง ปัญหาหลักๆ เกิดขึ้นเนื่องจากประสิทธิภาพที่ได้รับผลกระทบ

  • คุณ:has() :has() ไม่ได้ แต่คุณใช้ :has() ต่อกันได้ css :has(.a:has(.b)) { … }
  • ไม่มีการใช้องค์ประกอบสมมติภายใน :has() css :has(::after) { … } :has(::first-letter) { … }
  • จำกัดการใช้ :has() ภายในเงื่อนไขเท็จที่ยอมรับเฉพาะตัวเลือกแบบผสม css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • จำกัดการใช้ :has() หลังองค์ประกอบจำลอง css ::part(foo):has(:focus) { … }
  • การใช้ :visited จะเป็นเท็จเสมอ css :has(:visited) { … }

ดูเมตริกประสิทธิภาพจริงที่เกี่ยวข้องกับ :has() ได้ที่ข้อบกพร่องนี้ ขอขอบคุณ Byungwoo ที่แชร์ข้อมูลเชิงลึกและรายละเอียดเกี่ยวกับการนําไปใช้

เท่านี้ก็เรียบร้อย

เตรียมตัวให้พร้อมสำหรับ :has() บอกเพื่อนๆ เกี่ยวกับเรื่องนี้และแชร์โพสต์นี้ การเปลี่ยนแปลงนี้จะทําให้วิธีที่เราจัดการ CSS เปลี่ยนแปลงไปอย่างมาก

คุณสามารถดูตัวอย่างทั้งหมดได้ในคอลเล็กชัน CodePen นี้