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

Kể từ thời gian bắt đầu (Theo thuật ngữ CSS), chúng tôi đã làm việc với một tầng theo nhiều cách. Các kiểu của chúng ta tạo nên một "Biểu định kiểu xếp tầng". Bộ chọn của chúng tôi cũng đổ xuống theo tầng. Chúng có thể lệch sang một bên. Trong hầu hết các trường hợp, chúng sẽ xuống dưới. Nhưng không bao giờ đi lên. Trong nhiều năm, chúng tôi luôn tưởng tượng về "Bộ chọn cha mẹ". Và giờ đây, tính năng này cũng sắp ra mắt! Có dạng bộ chọn giả :has().

Lớp giả 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, bộ chọn này không chỉ là bộ chọn "gốc". Đó là một cách hay để tiếp thị sản phẩm. Cách không hấp dẫn như vậy có thể là bộ chọn "môi trường có điều kiện". Nhưng đó không phải lúc nào cũng vậy. Còn bộ chọn "gia đình" thì sao?

Hỗ trợ trình duyệt

Trước khi chúng ta tiếp tục, chúng tôi cần nhắc đến tính năng hỗ trợ trình duyệt. Nó vẫn chưa hoàn toàn chính xác. Nhưng nó sắp đạt được mục tiêu rồi. Firefox chưa được hỗ trợ, chúng tôi đã ra mắt tính năng này trong tương lai. Tuy nhiên, tính năng này đã có trong Safari và đã có trong Chromium 105. Tất cả bản minh hoạ trong bài viết này sẽ cho bạn biết liệu chúng có được hỗ trợ trong trình duyệt được sử dụng hay không.

Cách sử dụng :has

Vậy chiến lược này trông như thế nào? Hãy xem xét HTML sau đây với hai phần tử đồng cấp với lớp everybody. Bạn sẽ chọn một thiết bị có thành phần con cháu với 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 có một thành phần con cháu với lớp a-good-time.

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

Nhưng bạn có thể tiến xa hơn nhiều vì :has() mở ra rất nhiều cơ hội. Kể cả những gì có thể chưa được phát hiện. Hãy cân nhắc một số việc trong số này.

Chọn các phần tử figurefigcaption trực tiếp. css figure:has(> figcaption) { ... } Chọn những anchor không có thành phần con cháu SVG trực tiếp css a:not(:has(> svg)) { ... } Chọn những label có phần tử đồng cấp input trực tiếp. Đang lệch một bên! css label:has(+ input) { … } Chọn article mà trong đó thành phần con img không có văn bản alt css article:has(img:not([alt])) { … } Chọn documentElement nơi 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ố ít phần tử con 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 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 tất cả các phần tử tuỳ chỉnh a trong một đoạn văn có nhiều điều kiện a trực tiếp css p:has(+ hr) a:only-child { … }articlehrcss article:has(>h1):has(>h2) { … } Chọn một article có tiêu đề theo sau là phụ đề css article:has(> h1 + h2) { … } Chọn :root khi trạng thái tương tác được kích hoạt css :root:has(a:hover) { … } Chọn đoạn theo sau figure không có figcaption css figure:not(:has(figcaption)) + p { … }

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

Ví dụ

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

Thẻ

Xem bản minh hoạ thẻ cổ điển. Chúng tôi có thể hiển thị bất kỳ thông tin nào trong thẻ của mình, chẳng hạn như tiêu đề, phụ đề hoặc một số nội dung nghe nhìn. Đâ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 nội dung nghe nhìn? Đối với thiết kế này, thẻ có thể chia thành 2 cột. Trước đây, bạn có thể tạo một lớp mới để thể hiện hành vi này, ví dụ: card--with-media hoặc card--two-columns. Những tên lớp này không chỉ trở nên khó liên tưởng mà còn trở nên khó duy trì và khó nhớ.

Với :has(), bạn có thể phát hiện rằng thẻ chứa 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 của đố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ể thoả sức sáng tạo. Thẻ hiển thị nội dung "nổi bật" có thể điều chỉnh như thế nào trong bố cục? CSS này sẽ tạo một thẻ nổi bật có chiều rộng tối đa của 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);
}

Nếu một thẻ nổi bật có biểu ngữ đung đưa để thu hút sự chú ý thì sao?

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

Rất nhiều khả năng.

Biểu mẫu

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

Chẳng có lý do gì mà bạn không thể thêm một chút bất kỳ trang nhã khi người dùng tương tác với biểu mẫu của bạn. Hãy xem xét ví dụ sau. Xem khi bạn nhập giá trị hợp lệ cho tương tác vi mô. Giá trị :invalid sẽ khiến nhóm biểu mẫu bị rung lắc. 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 điều này trong các ví dụ về mã. Tuy nhiên, bạn có thể sử dụng :has() trong quy trình tài liệu như thế nào? Chương trình này đưa ra các ý tưởng về cách chúng tôi có thể tạo kiểu cho kiểu chữ trên nội dung đa phương tiện chẳng hạ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 có các hình. Khi không có figcaption, chúng sẽ nổi bên trong nội dung. Khi có figcaption, chúng sẽ chiếm toàn bộ chiều rộng và có thêm lề.

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

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

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

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

Tất cả những ví dụ này có điểm chung gì? Ngoài thực tế, chúng còn chỉ cho bạn cách sử dụng :has(), không có yêu cầu nào trong số đó yêu cầu sửa đổi tên lớp. Họ đề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 to 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ể chịu 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 sắp xếp tên lớp trong JavaScript, nhờ đó giảm khả năng xảy ra lỗi của nhà phát triển. Chúng ta đều ở đó khi đánh máy tên lớp và phải dùng cách giữ lại chúng trong tra cứu Object.

Đây là một ý kiến thú vị. Ý tưởng này có giúp chúng ta chuyển sang đánh dấu rõ ràng hơn và viết ít mã hơn không? Ít JavaScript hơn vì chúng tôi không thực hiện nhiều điều chỉnh JavaScript. Ít HTML hơn vì bạn không còn cần các lớp như card card--has-media, v.v.

Tư duy vượt giới hạn

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

Và điều đó mở ra những khả năng thú vị. Bạn có thể dùng biến này để truyền tải một biểu mẫu bằng 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 điện tử cổ điển thì sao? Cơ chế này dễ tạo hơn bằng :has(). Nếu dây bị lơ lửng thì trò chơi đã kết thúc. Có, chúng ta có thể tạo một số cơ chế trò chơi này bằng những trình 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 phải sử dụng các "thủ thuật" đá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 những hình ảnh này vào phiên bản chính thức, nhưng chúng nêu bật những cách mà bạn có thể sử dụng dữ liệu nguyên gốc. Chẳng hạn như có thể liên kết một :has().

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

Hiệu suất và hạn chế

Trước khi thoát, bạn không thể làm gì với :has()? Có một số hạn chế với :has(). Vấn đề chính phát sinh do lượt truy cập vào hiệu suất.

  • Bạn không thể :has() :has(). Tuy nhiên, bạn có thể liên kết một :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 giả lập, 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 có giá trị false css :has(:visited) { … }

Để biết các chỉ số hiệu suất thực tế liên quan đến :has(), hãy xem Sự cố này. Xin chân thành cảm ơn Byung Woo vì đã chia sẻ những thông tin chuyên sâu về quá trình triển khai.

Vậy là xong!

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

Tất cả bản minh hoạ đều có sẵn trong tính năng thu gọn CodePen này.