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

ตั้งแต่ช่วงเริ่มต้น (สำหรับ CSS) เราได้ทำงานกับ Cascade ในหลากหลายมุมมอง รูปแบบของเราประกอบด้วย "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() จะช่วยให้คุณทำสิ่งต่างๆ ได้ง่ายขึ้น เราสามารถเชื่อมต่อ Pseudo-class ที่เกี่ยวข้อง เช่น :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() เพื่อแสดงและซ่อนข้อความแสดงข้อผิดพลาดในช่องได้ด้วย ใช้กลุ่มช่อง "อีเมล" แล้วเพิ่มข้อความแสดงข้อผิดพลาดลงในกลุ่ม

<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 อยู่ จะใช้ความกว้างเต็มและจะมีระยะขอบเพิ่มเติม

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

ลองทำให้รูปแบบของคุณมีปฏิกิริยาต่อสถานะบางอย่างในมาร์กอัปของเราไหม ลองดูตัวอย่างที่มีแบบ "คลาสสิก" เลื่อนแถบนำทาง หากคุณมีปุ่มที่เปิดการนำทาง ปุ่มนั้นอาจใช้แอตทริบิวต์ 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() แล้ว ยังไม่จำเป็นต้องแก้ไขชื่อคลาสอีกด้วย โดยทั้งคู่ได้แทรกเนื้อหาใหม่และอัปเดตแอตทริบิวต์ 1 รายการ ซึ่งเป็นประโยชน์อย่างยิ่งของ :has() ตรงที่ช่วยลดข้อผิดพลาดของผู้ใช้ได้ ซึ่ง CSS ของ :has() จะมีหน้าที่รับผิดชอบในการปรับเปลี่ยนการแก้ไขใน 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;
}

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

และเพื่อความสนุกสนาน เรามาต่อกันที่เกมแบบต่อสายแบบ buzz สุดคลาสสิกไหม กลไกนี้สร้างได้ง่ายกว่าด้วย :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() ภายใน pseudos ที่ยอมรับเฉพาะตัวเลือกแบบผสม วันที่ 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 นี้