:has(): bộ chọn gia đình

Kể từ khi bắt đầu (theo thuật ngữ CSS), chúng ta đã làm việc với một thác nước theo nhiều nghĩa. Các kiểu của chúng ta tạo thành một "Biểu định kiểu xếp chồng". Và bộ chọn của chúng ta cũng có dạng thác nước. Chúng có thể di chuyển theo chiều ngang. Trong hầu hết các trường hợp, các giá trị này sẽ giảm xuống. Nhưng không bao giờ hướng lên trên. Trong nhiều năm, chúng tôi đã mơ ước về một "Công cụ chọn thành phần mẹ". Và giờ thì tính năng này đã ra mắt! Ở dạng bộ chọn giả :has().

Lớp giả lập CSS :has() đại diện cho một phần tử nếu bất kỳ bộ chọn nào được truyền dưới dạng tham số khớp với ít nhất một phần tử.

Tuy nhiên, đó không chỉ là bộ chọn "mẹ". Đó là một cách hay để tiếp thị sản phẩm. Cách không hấp dẫn lắm có thể là bộ chọn "môi trường có điều kiện". Nhưng cái tên đó không có âm hưởng như vậy. Còn bộ chọn "family" (gia đình) thì sao?

Hỗ trợ trình duyệt

Trước khi đi sâu hơn, chúng ta cần đề cập đến khả năng hỗ trợ trình duyệt. Chưa hoàn toàn như vậy. Nhưng thời điểm đó đang đến gần. Chưa hỗ trợ Firefox, nhưng chúng tôi đang lên kế hoạch hỗ trợ. Tuy nhiên, tính năng này đã có trong Safari và sẽ được phát hành trong Chromium 105. Tất cả các bản minh hoạ trong bài viết này sẽ cho bạn biết nếu chúng không được hỗ trợ trong trình duyệt đang dùng.

Cách sử dụng :has

Vậy giao diện này trông như thế nào? Hãy xem xét đoạn mã HTML sau đây với hai phần tử đồng cấp có lớp everybody. Bạn sẽ chọn phần tử có phần tử con thuộc lớp a-good-time như thế nào?

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

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

Với :has(), bạn có thể thực hiện việc đó bằng CSS sau.

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

Thao tác này sẽ chọn thực thể đầu tiên của .everybody và áp dụng animation.

Trong ví dụ này, phần tử có lớp everybody là mục tiêu. Điều kiện là có một phần tử con với lớp a-good-time.

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

Tuy nhiên, bạn có thể làm được nhiều việc hơn thế vì :has() mở ra nhiều cơ hội. Ngay cả những nội dung có thể chưa được phát hiện. Hãy cân nhắc một số yếu tố sau.

Chọn các phần tử figurefigcaption trực tiếp. css figure:has(> figcaption) { ... } Chọn anchor không có phần tử con SVG trực tiếp css a:not(:has(> svg)) { ... } Chọn label có phần tử đồng cấp input trực tiếp. Đi ngang! css label:has(+ input) { … } Chọn articleimg con không có văn bản alt css article:has(img:not([alt])) { … } Chọn documentElement có một số trạng thái trong DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } Chọn vùng chứa bố cục có số lượng phần tử con lẻ css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } Chọn tất cả các mục trong lưới không được di chuột qua css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Chọn vùng chứa chứa phần tử tuỳ chỉnh <todo-list> css main:has(todo-list) { ... } Chọn mọi a đơn lẻ trong một đoạn văn có phần tử hr đồng cấp trực tiếp css p:has(+ hr) a:only-child { … } Chọn một article đáp ứng nhiều điều kiện css article:has(>h1):has(>h2) { … } Kết hợp các phần tử đó. Chọn một article trong đó tiêu đề theo sau là phụ đề css article:has(> h1 + h2) { … } Chọn :root khi các trạng thái tương tác được kích hoạt css :root:has(a:hover) { … } Chọn đoạn văn bản theo sau là figure không có figcaption css figure:not(:has(figcaption)) + p { … }

Bạn có thể nghĩ ra trường hợp sử dụng thú vị nào cho :has()? Điều thú vị ở đây là bạn được khuyến khích phá vỡ mô hình tinh thần của mình. Điều này khiến bạn nghĩ "Tôi có thể tiếp cận các kiểu này theo cách khác không?".

Ví dụ

Hãy xem một số ví dụ về cách sử dụng tính năng này.

Thẻ

Tạo một bản minh hoạ thẻ cổ điển. Chúng ta có thể hiển thị bất kỳ thông tin nào trong thẻ, ví dụ: tiêu đề, phụ đề hoặc một số nội dung nghe nhìn. Dưới đây là thẻ cơ bản.

<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>

Điều gì xảy ra khi bạn muốn giới thiệu một số nội dung nghe nhìn? Đối với thiết kế này, thẻ có thể được chia thành hai cột. Trước đây, bạn có thể tạo một lớp mới để biểu thị hành vi này, chẳng hạn như card--with-media hoặc card--two-columns. Những tên lớp này không chỉ khó tạo mà còn khó duy trì và ghi nhớ.

Với :has(), bạn có thể phát hiện thẻ có một số nội dung nghe nhìn và thực hiện hành động thích hợp. Không cần tên lớp đối tượng sửa đổi.

<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>

Và bạn không cần phải để nó ở đó. Bạn có thể sáng tạo với ứng dụng này. Thẻ hiển thị nội dung "nổi bật" có thể điều chỉnh như thế nào trong một bố cục? CSS này sẽ tạo một thẻ nổi bật có chiều rộng bằng toàn bộ bố cục và đặt thẻ đó ở đầu lưới.

.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);
}

Điều gì sẽ xảy ra nếu một thẻ nổi bật có biểu ngữ nhấp nháy để thu hút sự chú ý?

<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;
}

Có rất nhiều khả năng.

Biểu mẫu

Còn biểu mẫu thì sao? Những kiểu tóc này nổi tiếng là khó tạo kiểu. Một ví dụ như vậy là định kiểu dữ liệu đầu vào và nhãn của dữ liệu đầu vào. Ví dụ: làm cách nào để chúng ta báo hiệu rằng một trường là hợp lệ? Với :has(), việc này trở nên dễ dàng hơn nhiều. Chúng ta có thể nối vào các lớp giả mạo biểu mẫu có liên quan, ví dụ: :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);
}

Hãy thử trong ví dụ sau: Thử nhập các giá trị hợp lệ và không hợp lệ, đồng thời bật và tắt tiêu điểm.

Bạn cũng có thể sử dụng :has() để hiển thị và ẩn thông báo lỗi cho một trường. Lấy nhóm trường "email" và thêm thông báo lỗi vào nhóm đó.

<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>

Theo mặc định, bạn sẽ ẩn thông báo lỗi.

.form-group__error {
  display: none;
}

Tuy nhiên, khi trường trở thành :invalid và không được lấy làm tiêu điểm, bạn có thể hiển thị thông báo mà không cần thêm tên lớp.

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

Không có lý do gì khiến bạn không thể thêm một chút tinh tế và ngẫu hứng cho trải nghiệm tương tác của người dùng với biểu mẫu. Hãy xem xét ví dụ sau. Hãy xem khi bạn nhập một giá trị hợp lệ cho lượt tương tác vi mô. Giá trị :invalid sẽ khiến nhóm biểu mẫu rung. Tuy nhiên, chỉ khi người dùng không có lựa chọn ưu tiên về chuyển động.

Nội dung

Chúng ta đã đề cập đến vấn đề này trong các ví dụ về mã. Tuy nhiên, làm cách nào để bạn có thể sử dụng :has() trong luồng tài liệu? Ví dụ: công cụ này đưa ra ý tưởng về cách chúng ta có thể tạo kiểu chữ xung quanh nội dung nghe nhìn.

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%;
}

Ví dụ này chứa các hình. Khi không có figcaption, các thành phần này sẽ nổi trong nội dung. Khi có figcaption, các thành phần này sẽ chiếm toàn bộ chiều rộng và có thêm khoảng đệm.

Phản ứng với trạng thái

Bạn có thể làm cho các kiểu phản ứng với một số trạng thái trong mã đánh dấu của chúng tôi. Hãy xem xét ví dụ về thanh điều hướng trượt "cổ điển". Nếu bạn có một nút bật/tắt mở thanh điều hướng, thì nút đó có thể sử dụng thuộc tính aria-expanded. Bạn có thể sử dụng JavaScript để cập nhật các thuộc tính thích hợp. Khi aria-expandedtrue, hãy sử dụng :has() để phát hiện điều này và cập nhật kiểu cho thanh điều hướng trượt. JavaScript thực hiện phần việc của mình và CSS có thể làm những gì nó muốn với thông tin đó. Bạn không cần phải xáo trộn mã đánh dấu hoặc thêm tên lớp bổ sung, v.v. (Lưu ý: Đây không phải là ví dụ sẵn sàng để phát hành).

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

:has có thể giúp tránh lỗi người dùng không?

Điểm chung của tất cả các ví dụ này là gì? Ngoài việc cho thấy cách sử dụng :has(), không có phương thức nào yêu cầu sửa đổi tên lớp. Mỗi hàm này đều chèn nội dung mới và cập nhật một thuộc tính. Đây là một lợi ích lớn của :has(), vì nó có thể giúp giảm thiểu lỗi của người dùng. Với :has(), CSS có thể đảm nhận trách nhiệm điều chỉnh các nội dung sửa đổi trong DOM. Bạn không cần phải điều phối tên lớp trong JavaScript, nhờ đó giảm thiểu khả năng xảy ra lỗi cho nhà phát triển. Chúng ta đều đã từng mắc lỗi chính tả tên lớp và phải chuyển tên lớp đó vào mục tra cứu Object.

Đây là một ý tưởng thú vị và liệu nó có giúp chúng ta tạo ra mã nguồn gọn gàng hơn và ít mã hơn không? Ít JavaScript hơn vì chúng ta không điều chỉnh JavaScript nhiều như trước. Ít HTML hơn vì bạn không cần các lớp như card card--has-media, v.v.

Tư duy sáng tạo

Như đã đề cập ở trên, :has() khuyến khích bạn phá vỡ mô hình tinh thần. Đây là cơ hội để thử nhiều cách. Một cách để thử và đẩy ranh giới là chỉ tạo cơ chế trò chơi bằng CSS. Ví dụ: bạn có thể tạo một cơ chế dựa trên bước bằng các biểu mẫu và 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;
}

Điều này mở ra nhiều khả năng thú vị. Bạn có thể sử dụng hàm đó để di chuyển qua một biểu mẫu có các phép biến đổi. Lưu ý: Bạn nên xem bản minh hoạ này trong một thẻ trình duyệt riêng.

Còn trò chơi dây điện rung kinh điển thì sao? Bạn có thể dễ dàng tạo cơ chế này bằng :has(). Nếu bạn di chuột qua dây, trò chơi sẽ kết thúc. Có, chúng ta có thể tạo một số cơ chế trò chơi này bằng các thành phần như toán tử kết hợp đồng cấp (+~). Tuy nhiên, :has() là một cách để đạt được những kết quả tương tự mà không cần sử dụng các "mẹo" đánh dấu thú vị. Lưu ý: Bạn nên xem bản minh hoạ này trong một thẻ trình duyệt riêng.

Mặc dù bạn sẽ không sớm đưa các thành phần này vào phiên bản chính thức, nhưng chúng nêu bật những cách bạn có thể sử dụng thành phần gốc. Chẳng hạn như có thể tạo chuỗi :has().

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

Hiệu suất và giới hạn

Trước khi kết thúc, bạn không thể làm gì với :has()? :has() có một số hạn chế. Các lỗi chính phát sinh do các lần giảm hiệu suất.

  • Bạn không thể :has() một :has(). Tuy nhiên, bạn có thể tạo chuỗi :has(). css :has(.a:has(.b)) { … }
  • Không sử dụng phần tử giả trong :has() css :has(::after) { … } :has(::first-letter) { … }
  • Hạn chế sử dụng :has() bên trong các giá trị đại diện chỉ chấp nhận bộ chọn phức hợp css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • Hạn chế sử dụng :has() sau phần tử giả css ::part(foo):has(:focus) { … }
  • Việc sử dụng :visited sẽ luôn là sai css :has(:visited) { … }

Để biết các chỉ số hiệu suất thực tế liên quan đến :has(), hãy xem Lỗi này. Cảm ơn Byungwoo đã chia sẻ những thông tin chi tiết và thông tin chi tiết này về việc triển khai.

Vậy là xong!

Chuẩn bị sẵn sàng cho :has(). Hãy chia sẻ bài đăng này với bạn bè của bạn. Đây sẽ là một bước ngoặt trong cách chúng ta tiếp cận CSS.

Tất cả các bản minh hoạ đều có trong bộ sưu tập CodePen này.