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

Kể từ khi thời gian bắt đầu (Về mặt CSS), chúng tôi đã làm việc với một thác nước theo nhiều khía cạnh khác nhau. Các kiểu của chúng tôi sẽ tạo thành một "Biểu định kiểu xếp chồng". Và các bộ chọn của chúng tôi cũng thác theo. Chúng có thể lệch sang một bên. Trong hầu hết các trường hợp, quảng cáo đi xuống. Nhưng không bao giờ đi lên. Trong nhiều năm, chúng tôi đã tưởng tượng ra "Bộ chọn cha mẹ". Và bây giờ, tính năng này đã có! Có hình dạng của 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ử.

Nhưng đó không chỉ là "cha mẹ" . Đó là một cách hay để tiếp thị sản phẩm. Cách không bắt mắt có thể là "môi trường có điều kiện" . Nhưng chiếc nhẫn đó không giống nhau. Thế còn "gia đình" thì sao bộ chọn?

Hỗ trợ trình duyệt

Trước khi đi sâu hơn, bạn nên đề cập đến tính năng hỗ trợ trình duyệt. Chúng tôi vẫn chưa hoàn tất. Nhưng ngày càng gần hơn. Chưa hỗ trợ Firefox, trình duyệt này đang trong lộ trình ra mắt. Tuy nhiên, tệp này đã có trong Safari và sẽ được phát hành 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ó không được hỗ trợ trong trình duyệt bạn sử dụng hay không.

Cách sử dụng :has

Vậy trang này hoạt động như thế nào? Hãy xem xét HTML sau đây với 2 phần tử đồng cấp có lớp everybody. Bạn sẽ chọn lớp có thành phần 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ể làm việc đó bằng CSS sau.

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

Thao tác này 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 thành phần con thuộc lớp a-good-time.

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

Tuy nhiên, bạn có thể tiến xa hơn thế vì :has() mở ra rất nhiều cơ hội. Thậm chí cả những nội dung có thể chưa được khám phá ra. Hãy cân nhắc một số yếu tố trong số này.

Chọn các phần tử figurefigcaption trực tiếp. css figure:has(> figcaption) { ... } Chọn các anchor không có thành phần con cháu SVG trực tiếp css a:not(:has(> svg)) { ... } Chọn các label có đồng cấp input trực tiếp. Đang đi sang một bên! css label:has(+ input) { … } Chọn các article mà 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 hiển thị 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 một lưới không di chuột css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } Chọn vùng chứa chứa phần tử tùy chỉnh <todo-list> css main:has(todo-list) { ... } Chọn mỗi a một mình trong một đoạn có phần tử đồng cấp trực tiếp hr 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) { … } Hãy kết hợp. Chọn một article có tiêu đề kèm theo 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 theo sau figure không có figcaption css figure:not(:has(figcaption)) + p { … }

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

Ví dụ

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

Thẻ

Xem bản minh hoạ về thẻ cổ điển. Chúng tôi có thể hiển thị bất kỳ thông tin nào trong thẻ, ví dụ: tiêu đề, phụ đề hoặc nội dung nghe nhìn nào đó. Sau đâ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ể 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ó gợi lên mà còn trở nên khó duy trì và nhớ.

Với :has(), bạn có thể phát hiện xem thẻ có một số nội dung nghe nhìn và thao tác 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 để dữ liệu ở đó. Bạn có thể sáng tạo thông qua ứng dụng này. Cách một thẻ hiển thị nội dung "nổi bật" có thể điều chỉnh trong bố cục? CSS này sẽ làm cho thẻ nổi bật có toàn bộ chiều rộng 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);
}

Điều gì sẽ xảy ra nếu một thẻ nổi bật có biểu ngữ lắc lư để 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;
}

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

Biểu mẫu

Biểu mẫu thì sao? Loại phụ kiện này nổi tiếng là khó tạo kiểu. Một ví dụ cho trường hợp này là định kiểu đầu vào và nhãn của chúng. Ví dụ: làm cách nào để chúng tôi báo hiệu một trường là hợp lệ? Với :has(), việc này sẽ trở nên dễ dàng hơn nhiều. Chúng ta có thể nối vào các lớp giả của biểu mẫu phù hợp, 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ể sử dụng :has() để hiện và ẩn thông báo lỗi của một trường. Hãy 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 sẽ ẩn thông báo lỗi.

.form-group__error {
  display: none;
}

Tuy nhiên, khi trường này trở thành :invalid và không được đặt tiêu đ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;
}

Không có lý do gì khiến bạn không thể thêm một chút biến tấu 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 rung chuyển. 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, làm cách nào để bạn có thể sử dụng :has() trong quy trình tài liệu? Ví dụ: công cụ này đề xuất các ý tưởng về cách chúng tôi có thể tạo kiểu chữ dựa trên 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 có chứa hình. Khi không có figcaption, chúng sẽ nổi 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 thế nào để làm cho các kiểu của bạn tương ứng với một trạng thái nào đó trong mã đánh dấu của chúng tôi. Hãy xem xét một ví dụ với từ khoá "cổ điển" thanh điều hướng trượt. Nếu bạn có nút bật/tắt mở điều hướng, 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 sẽ thực hiện vai trò của mình và CSS có thể làm những gì họ 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ụ về bản phát hành 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 cho người dùng không?

Tất cả những ví dụ này có điểm chung gì? Bên cạnh việc chỉ ra cách sử dụng :has(), bạn không cần sửa đổi tên lớp. Cả hai đề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 sắp xếp các tên lớp trong JavaScript, giảm khả năng xảy ra lỗi của nhà phát triển. Ai trong chúng ta cũng từng xảy ra lỗi khi đánh máy tên lớp và phải dùng cách này để giữ lại tên lớp trong Object tra cứu.

Đây là một ý tưởng thú vị và có giúp chúng tôi hướng đến việc đánh dấu sạch hơn và giảm bớt mã 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.

Suy nghĩ mới lạ

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ử nhiều điều. Một cách để nỗ lực vượt qua những ranh giới đó là xây dựng cơ chế trò chơi chỉ với CSS. Bạn có thể tạo một cơ chế theo bước với biểu mẫu và CSS chẳng hạn.

<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ể sử dụng cách đó để truyền tải một biểu mẫu bằng phép biến đổi. Xin lưu ý rằng tốt nhất bạn nên xem bản minh hoạ này trong một thẻ trình duyệt riêng.

Và để cho vui vẻ, còn trò chơi truyền hình cổ điển thì sao? Việc tạo cơ chế sẽ dễ dàng hơn nhờ :has(). Nếu dây bay qua thì trò chơi kết thúc. Có, chúng ta có thể tạo ra một số cơ chế trò chơi bằng những thứ như các tổ hợp đồng cấp (+~). Tuy nhiên, :has() là một cách để đạt được 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ị. Xin lưu ý rằng tốt nhất 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 phần này vào phiên bản chính thức, nhưng chúng sẽ nêu bật những cách mà bạn có thể sử dụng phần tử gốc. Chẳng hạn như khả năng 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 tiếp tục, bạn không thể làm gì với :has()? Có một số hạn chế với :has(). Các lượt nhấp chính phát sinh do các lượt truy cập hiệu suất.

  • Bạn không thể :has() một :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ế việc sử dụng :has() bên trong các phần tử giả 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 sai css :has(:visited) { … }

Để biết các chỉ số về hiệu suất thực tế liên quan đến :has(), hãy xem Lỗi trục trặc này. Xin chân thành cảm ơn Byung Woo đã chia sẻ những thông tin và thông tin 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ề vấn đề này và chia sẻ bài đăng này. Đây sẽ là một yếu tố độ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 bộ sưu tập CodePen này.