polyfill truy vấn bên trong vùng chứa

Gerald Monaco
Gerald Monaco

Truy vấn vùng chứa là một tính năng CSS mới cho phép bạn viết logic tạo kiểu nhắm đến các tính năng của phần tử mẹ (ví dụ: chiều rộng hoặc chiều cao) để tạo kiểu cho các phần tử con. Gần đây, chúng tôi đã phát hành một bản cập nhật lớn cho polyfill, trùng với trang hỗ trợ trong trình duyệt.

Trong bài đăng này, bạn sẽ có thể xem xét cách hoạt động của polyfill, những thách thức mà polyfill vượt qua và các phương pháp hay nhất khi sử dụng polyfill để mang lại trải nghiệm người dùng tuyệt vời cho khách truy cập.

Tìm hiểu sâu

Chuyển đổi

Khi trình phân tích cú pháp CSS bên trong trình duyệt gặp phải một quy tắc at-rule không xác định, chẳng hạn như quy tắc @container hoàn toàn mới, trình phân tích cú pháp sẽ loại bỏ quy tắc đó như thể quy tắc đó chưa từng tồn tại. Do đó, việc đầu tiên và quan trọng nhất mà polyfill phải làm là chuyển đổi truy vấn @container thành một truy vấn sẽ không bị loại bỏ.

Bước đầu tiên trong quá trình chuyển đổi mã là chuyển đổi quy tắc @container cấp cao nhất thành truy vấn @media. Điều này chủ yếu là đảm bảo nội dung vẫn được nhóm lại với nhau. Ví dụ: khi sử dụng các API CSSOM và khi xem nguồn CSS.

Trước
@container (width > 300px) {
  /* content */
}
Sau
@media all {
  /* content */
}

Trước khi có truy vấn vùng chứa, CSS không có cách nào để tác giả tự ý bật hoặc vô hiệu các nhóm quy tắc. Để polyfill hành vi này, bạn cũng cần chuyển đổi các quy tắc bên trong truy vấn vùng chứa. Mỗi @container được cấp một mã nhận dạng duy nhất (ví dụ: 123). Mã này được dùng để biến đổi từng bộ chọn sao cho bộ chọn chỉ áp dụng khi phần tử có thuộc tính cq-XYZ bao gồm mã nhận dạng này. Thuộc tính này sẽ do polyfill đặt trong thời gian chạy.

Trước
@container (width > 300px) {
  .card {
    /* ... */
  }
}
Sau
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

Hãy lưu ý việc sử dụng lớp giả :where(...). Thông thường, việc thêm một bộ chọn thuộc tính bổ sung sẽ làm tăng độ cụ thể của bộ chọn. Với lớp giả, bạn có thể áp dụng điều kiện bổ sung mà vẫn giữ nguyên tính cụ thể ban đầu. Để biết lý do tại sao điều này lại quan trọng, hãy xem xét ví dụ sau:

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

Với CSS này, phần tử có lớp .card phải luôn có color: red, vì quy tắc sau sẽ luôn ghi đè quy tắc trước đó bằng cùng một bộ chọn và mức độ cụ thể. Do đó, việc chuyển đổi quy tắc đầu tiên và thêm một bộ chọn thuộc tính bổ sung mà không có :where(...) sẽ làm tăng độ cụ thể và khiến color: blue được áp dụng nhầm.

Tuy nhiên, lớp giả :where(...) khá mới. Đối với những trình duyệt không hỗ trợ tính năng này, polyfill cung cấp một giải pháp an toàn và dễ dàng: bạn có thể cố ý tăng độ cụ thể của các quy tắc bằng cách thêm thủ công bộ chọn :not(.container-query-polyfill) giả vào các quy tắc @container của bạn:

Trước
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
Sau
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

Việc này có một số lợi ích:

  • Bộ chọn trong CSS nguồn đã thay đổi, vì vậy sự khác biệt về tính cụ thể được thể hiện rõ ràng. Điều này cũng đóng vai trò là tài liệu để bạn biết những gì bị ảnh hưởng khi không cần hỗ trợ giải pháp hoặc polyfill nữa.
  • Tính cụ thể của các quy tắc sẽ luôn giống nhau vì polyfill không thay đổi các quy tắc đó.

Trong quá trình chuyển đổi, polyfill sẽ thay thế phần tử giả lập này bằng bộ chọn thuộc tính có cùng mức độ cụ thể. Để tránh mọi sự cố, polyfill sử dụng cả hai bộ chọn: bộ chọn nguồn gốc được dùng để xác định xem phần tử có nhận được thuộc tính polyfill hay không và bộ chọn được chuyển đổi được dùng để tạo kiểu.

Phần tử giả

Bạn có thể tự hỏi: nếu polyfill đặt một số thuộc tính cq-XYZ trên một phần tử để bao gồm mã nhận dạng vùng chứa duy nhất 123, thì làm cách nào để hỗ trợ các phần tử mô phỏng không thể đặt thuộc tính trên đó?

Phần tử giả luôn liên kết với một phần tử thực trong DOM, được gọi là phần tử gốc. Trong quá trình chuyển đổi mã, bộ chọn có điều kiện sẽ được áp dụng cho phần tử thực này:

Trước
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
Sau
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

Thay vì được chuyển đổi thành #foo::before:where([cq-XYZ~="123"]) (không hợp lệ), bộ chọn có điều kiện sẽ được chuyển sang cuối phần tử ban đầu, #foo.

Tuy nhiên, đó không phải là tất cả những gì cần thiết. Vùng chứa không được phép sửa đổi bất kỳ nội dung nào không nằm bên trong vùng chứa đó (và vùng chứa không được ở bên trong chính vùng chứa đó), nhưng hãy nhớ rằng đó chính xác là những gì sẽ xảy ra nếu #foo chính là phần tử vùng chứa được truy vấn. Thuộc tính #foo[cq-XYZ] sẽ bị thay đổi do nhầm lẫn và mọi quy tắc #foo sẽ bị áp dụng do nhầm lẫn.

Để khắc phục vấn đề này, polyfill thực sự sử dụng hai thuộc tính: một thuộc tính chỉ có thể được phần tử mẹ áp dụng cho một phần tử và một thuộc tính mà phần tử có thể tự áp dụng cho chính nó. Thuộc tính sau được dùng cho bộ chọn nhắm đến các phần tử giả.

Trước
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
Sau
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

Vì một vùng chứa sẽ không bao giờ áp dụng thuộc tính đầu tiên (cq-XYZ-A) cho chính nó, nên bộ chọn đầu tiên sẽ chỉ khớp nếu một vùng chứa mẹ khác đáp ứng các điều kiện về vùng chứa và áp dụng thuộc tính đó.

Đơn vị tương đối của vùng chứa

Truy vấn vùng chứa cũng đi kèm với một vài đơn vị mới mà bạn có thể sử dụng trong CSS, chẳng hạn như cqwcqh cho 1% chiều rộng và chiều cao (tương ứng) của vùng chứa gốc thích hợp gần nhất. Để hỗ trợ các giá trị này, đơn vị được chuyển đổi thành biểu thức calc(...) bằng cách sử dụng Thuộc tính tuỳ chỉnh của CSS. Trình bổ trợ sẽ đặt giá trị cho các thuộc tính này thông qua các kiểu nội tuyến trên phần tử vùng chứa.

Trước
.card {
  width: 10cqw;
  height: 10cqh;
}
Sau
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

Ngoài ra còn có các đơn vị logic, như cqicqb cho kích thước cùng dòng và kích thước khối (tương ứng). Các trục này phức tạp hơn một chút vì trục nội tuyến và trục khối được xác định bằng writing-mode của phần tử sử dụng đơn vị, chứ không phải phần tử được truy vấn. Để hỗ trợ việc này, polyfill áp dụng kiểu cùng dòng cho bất kỳ phần tử nào có writing-mode khác với phần tử mẹ.

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

Giờ đây, bạn có thể chuyển đổi các đơn vị thành Thuộc tính tuỳ chỉnh CSS thích hợp như trước.

Thuộc tính

Truy vấn vùng chứa cũng thêm một số thuộc tính CSS mới như container-typecontainer-name. Vì không thể sử dụng các API như getComputedStyle(...) với các thuộc tính không xác định hoặc không hợp lệ, nên các thuộc tính này cũng được chuyển đổi thành Thuộc tính tuỳ chỉnh CSS sau khi được phân tích cú pháp. Nếu không thể phân tích cú pháp một tài sản (ví dụ: do tài sản chứa giá trị không hợp lệ hoặc không xác định), thì tài sản đó chỉ còn để trình duyệt xử lý.

Trước
.card {
  container-name: card-container;
  container-type: inline-size;
}
Sau
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

Các thuộc tính này được chuyển đổi bất cứ khi nào được phát hiện, cho phép polyfill hoạt động tốt với các tính năng CSS khác như @supports. Chức năng này là cơ sở của các phương pháp hay nhất để sử dụng polyfill, như được đề cập bên dưới.

Trước
@supports (container-type: inline-size) {
  /* ... */
}
Sau
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

Theo mặc định, các thuộc tính tuỳ chỉnh CSS được kế thừa, nghĩa là mọi phần tử con của .card sẽ nhận giá trị của --cq-XYZ-container-name--cq-XYZ-container-type. Đó chắc chắn không phải là cách hoạt động của các thuộc tính gốc. Để giải quyết vấn đề này, polyfill sẽ chèn quy tắc sau đây trước mọi kiểu của người dùng, đảm bảo rằng mọi phần tử đều nhận được giá trị ban đầu, trừ phi một quy tắc khác cố ý ghi đè.

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

Các phương pháp hay nhất

Mặc dù dự kiến hầu hết khách truy cập sẽ sớm chạy các trình duyệt có hỗ trợ truy vấn vùng chứa tích hợp, nhưng điều quan trọng là bạn vẫn phải mang lại trải nghiệm tốt cho những khách truy cập còn lại.

Trong lần tải đầu tiên, có rất nhiều việc cần làm trước khi polyfill có thể bố trí trang:

  • Bạn cần tải và khởi chạy polyfill.
  • Bạn cần phân tích cú pháp và chuyển đổi trang tính kiểu. Vì không có API nào để truy cập vào nguồn thô của một kiểu phông chữ bên ngoài, nên bạn có thể cần phải tìm nạp lại không đồng bộ, mặc dù lý tưởng nhất là chỉ từ bộ nhớ đệm của trình duyệt.

Nếu polyfill không giải quyết cẩn thận những vấn đề này, thì polyfill có thể làm giảm Các chỉ số quan trọng về trang web.

Để giúp bạn dễ dàng mang đến cho khách truy cập một trải nghiệm thú vị, polyfill đã được thiết kế để ưu tiên Độ trễ đầu vào đầu tiên (FID)Điểm số tổng hợp về mức thay đổi bố cục (CLS), có thể đánh mất lợi ích của Nội dung lớn nhất hiển thị (LCP). Cụ thể, polyfill không đảm bảo rằng các truy vấn vùng chứa của bạn sẽ được đánh giá trước lần vẽ đầu tiên. Điều này có nghĩa là để mang lại trải nghiệm tốt nhất cho người dùng, bạn phải đảm bảo rằng mọi nội dung có kích thước hoặc vị trí bị ảnh hưởng khi sử dụng truy vấn vùng chứa đều bị ẩn cho đến khi polyfill tải và chuyển đổi CSS của bạn. Một cách để thực hiện việc này là sử dụng quy tắc @supports:

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

Bạn nên kết hợp hiệu ứng này với ảnh động tải CSS thuần tuý, được đặt ở vị trí tuyệt đối trên nội dung (ẩn) của bạn để cho khách truy cập biết rằng có điều gì đó đang diễn ra. Bạn có thể xem bản minh hoạ đầy đủ về phương pháp này tại đây.

Bạn nên sử dụng phương pháp này vì một số lý do:

  • Trình tải CSS thuần tuý giúp giảm thiểu hao tổn cho người dùng sử dụng trình duyệt mới hơn, đồng thời cung cấp phản hồi nhẹ cho những người dùng sử dụng trình duyệt cũ và mạng chậm hơn.
  • Bằng cách kết hợp vị trí tuyệt đối của trình tải với visibility: hidden, bạn có thể tránh được sự thay đổi bố cục.
  • Sau khi polyfill tải, điều kiện @supports này sẽ ngừng truyền và nội dung của bạn sẽ hiển thị.
  • Trên các trình duyệt có hỗ trợ tích hợp cho các truy vấn vùng chứa, điều kiện sẽ không bao giờ vượt qua, và do đó trang sẽ được hiển thị trong lần hiển thị đầu tiên như dự kiến.

Kết luận

Nếu bạn muốn sử dụng truy vấn vùng chứa trên các trình duyệt cũ hơn, hãy dùng thử polyfill. Đừng ngại báo cáo vấn đề nếu bạn gặp bất kỳ vấn đề nào.

Chúng tôi rất mong được xem và trải nghiệm những điều tuyệt vời mà bạn sẽ tạo ra bằng công cụ này.