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 mã nguồn
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 nội dung 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. Việc này chủ yếu giúp đảm bảo rằng 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.
@container (width > 300px) { /* content */ }
@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ả tuỳ ý bật hoặc tắt 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.
@container (width > 300px) { .card { /* ... */ } }
@media all { .card:where([cq-XYZ~="123"]) { /* ... */ } }
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 trong khi vẫn giữ nguyên tính cụ thể ban đầu. Để biết lý do tại sao điều này rất quan trọng, hãy xem 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 không có :where(...)
sẽ làm tăng độ cụ thể và khiến color: blue
được áp dụng không chính xác.
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 bộ chọn :not(.container-query-polyfill)
giả vào các quy tắc @container
theo cách thủ công:
@container (width > 300px) { .card { color: blue; } } .card { color: red; }
@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ề mức độ cụ thể sẽ hiển thị 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 mã, 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:
@container (width > 300px) { #foo::before { /* ... */ } }
@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, đó chưa 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 trong vùng chứa đó (và vùng chứa không được nằm bên trong chính nó), nhưng hãy xem xét rằng đó chính xác là điều sẽ xảy ra nếu #foo
chính là phần tử vùng chứa đang đượ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ả.
@container (width > 300px) { #foo, #foo::before { /* ... */ } }
@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 của 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ư cqw
và cqh
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 thuộc tính 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 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.
.card { width: 10cqw; height: 10cqh; }
.card { width: calc(10 * --cq-XYZ-cqw); height: calc(10 * --cq-XYZ-cqh); }
Ngoài ra còn có các đơn vị logic, chẳng hạn như cqi
và cqb
cho kích thước nội tuyến 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 nội tuyến cho mọi phần tử 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-type
và container-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 thuộc tính (ví dụ: vì thuộc tính đó chứa một giá trị không hợp lệ hoặc không xác định), thì thuộc tính đó sẽ được để nguyên để trình duyệt xử lý.
.card { container-name: card-container; container-type: inline-size; }
.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.
@supports (container-type: inline-size) { /* ... */ }
@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
và --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 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 các trang 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 trải nghiệm dễ chịu cho khách truy cập, polyfill được thiết kế để ưu tiên Thời gian phản hồi lần tương tác đầu tiên (FID) và Mức thay đổi bố cục tích luỹ (CLS), có thể phải hy sinh Thời gian hiển thị nội dung lớn nhất (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 việc 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 truy vấn vùng chứa, điều kiện sẽ không bao giờ được truyền, vì vậy, trang sẽ hiển thị trên lần vẽ đầ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ãy 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.