Polyfill zapytania w kontenerze

Gerald Monaco
Gerald Monaco

Zapytania o kontenery to nowa funkcja CSS, która umożliwia napisanie logiki stylu ukierunkowanej na cechy elementu nadrzędnego (np. jego szerokość lub wysokość) w celu określenia stylu jego elementów podrzędnych. Niedawno opublikowaliśmy dużą aktualizację polyfilla, która zbiegła się z dodaniem strony z informacjami o obsługiwanych przeglądarkach.

Z tego posta dowiesz się, jak działa kod polyfill i jak radzi sobie z nim pokonany kod, a także poznasz sprawdzone metody związane z korzystaniem z niego w celu zapewnienia użytkownikom pozytywnych wrażeń.

Na zapleczu

Transpilacja

Gdy parser CSS w przeglądarce natrafi na nieznaną regułę „ad”, np. nową regułę @container, odrzuci ją, jakby nigdy nie istniała. Dlatego pierwszą i najważniejszą rzeczą, którą musi zrobić polyfill, jest przetłumaczenie zapytania @container na coś, czego nie można odrzucić.

Pierwszym krokiem w transpilacji jest przekształcenie reguły @container najwyższego poziomu w zapytanie @media. Głównie zapewnia to utrzymanie grupy treści w jednym miejscu. Na przykład przy korzystaniu z interfejsów API CSSOM i podczas wyświetlania źródła CSS.

Przed
@container (width > 300px) {
  /* content */
}
Po
@media all {
  /* content */
}

Przed zapytaniem o kontenery autor nie miał możliwości arbitralnego włączania i wyłączania grup reguł. Aby wypełnić to zachowanie, reguły w zapytaniu kontenera muszą też zostać przekształcone. Każdy element @container otrzymuje własny unikalny identyfikator (np. 123), który służy do przekształcenia każdego selektora w taki sposób, by był stosowany tylko wtedy, gdy element ma atrybut cq-XYZ zawierający ten identyfikator. Ten atrybut zostanie ustawiony przez polyfill w czasie wykonywania.

Przed
@container (width > 300px) {
  .card {
    /* ... */
  }
}
Po
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

Zwróć uwagę na użycie pseudoklasy :where(...). Dodanie dodatkowego selektora atrybutu zwiększy szczególność selektora. Dzięki pseudoklasie można zastosować dodatkowy warunek, zachowując pierwotną specyficzność. Aby przekonać się, dlaczego to takie ważne, zapoznaj się z tym przykładem:

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

.card {
  color: red;
}

W tym przypadku element z klasą .card powinien zawsze mieć wartość color: red, ponieważ późniejsza reguła zawsze zastąpi poprzednią regułę z tym samym selektorem i tą samą specyfiką. Przetłumaczenie pierwszej reguły i uwzględnienie dodatkowego selektora atrybutu bez :where(...) zwiększyłoby specyficzność i spowodowałoby błędne zastosowanie reguły color: blue.

Jednak pseudoklasa :where(...) jest dosyć nowa. W przypadku przeglądarek, które nie obsługują tej funkcji, polyfill zapewnia bezpieczne i łatwe obejście problemu: możesz celowo zwiększyć specyficzność reguł, ręcznie dodając do reguł @container selektor typu :not(.container-query-polyfill):

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

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

.card {
  color: red;
}

Daje to kilka korzyści:

  • Selektor w źródłowym pliku CSS uległ zmianie, więc różnica w konkretności jest wyraźnie widoczna. Jest to też dokumentacja, która pozwala określić, co jest objęte rozwiązaniem, gdy nie będziesz już potrzebować rozwiązania obejściowego ani polyfill.
  • Specyficzność reguł będzie zawsze taka sama, ponieważ polyfill jej nie zmienia.

Podczas transpilacji polyfill zastąpi tę wartość zastępczą selektorem atrybutu o tej samej specyficzności. Aby uniknąć niespodzianek, kod polyfill używa obu selektorów: do określenia, czy element powinien otrzymać atrybut polyfill, używany jest oryginalny selektor źródła, a selektor po transpilacji służy do określania stylu.

Elementy pseudo

Możesz się zastanawiać, jak to możliwe, że jeśli funkcja polyfill ustawia w elemencie atrybut cq-XYZ, aby zawierał unikalny identyfikator kontenera 123, to jak pseudoelementy, które nie mogą mieć ustawionych atrybutów, mogą być obsługiwane.

Pseudoelementy są zawsze powiązane z prawdziwym elementem w DOM, nazywanym elementem źródłowym. Podczas transpilacji selektor warunkowy jest stosowany do tego rzeczywistego elementu:

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

Zamiast przekształcać się w #foo::before:where([cq-XYZ~="123"]) (co byłoby nieprawidłowe), selektor warunkowy jest przenoszony na koniec elementu źródłowego, czyli #foo.

To jednak nie wszystko. Kontener nie może modyfikować niczego, co nie jest zawarowane w jego wnętrzu (a kontener nie może znajdować się wewnątrz samego siebie), ale zastanów się, co by się stało, gdyby #foo był elementem kontenera, którego dotyczy zapytanie. Atrybut #foo[cq-XYZ] zostanie błędnie zmieniony, a wszystkie reguły #foo zostaną błędnie zastosowane.

Aby to poprawić, kod polyfill faktycznie używa 2 atrybutów: jednego, który element nadrzędny może zastosować tylko do elementu, a drugiego – samego elementu. Ten drugi atrybut jest używany w selektorach, które będą kierować na pseudoelementy.

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

Ponieważ kontener nigdy nie stosuje pierwszego atrybutu (cq-XYZ-A) do siebie, pierwszy selektor zostanie dopasowany tylko wtedy, gdy inny kontener nadrzędny spełni warunki kontenera i go zastosował.

Jednostki względne kontenera

Zapytania dotyczące kontenerów zawierają również kilka nowych jednostek, które możesz wykorzystać w kodzie CSS, np. cqw i cqh dla 1% szerokości i wysokości (odpowiednio) najbliższego odpowiedniego kontenera nadrzędnego. Aby to umożliwić, jednostka jest przekształcana w wyrażenie calc(...) za pomocą właściwości niestandardowych w CSS. Polyfill ustawia wartości tych właściwości za pomocą stylów wbudowanych w elemencie kontenera.

Przed
.card {
  width: 10cqw;
  height: 10cqh;
}
Po
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

Istnieją też jednostki logiczne, takie jak cqicqb, odpowiednio dla rozmiaru wstawionego i blokowego. Są one nieco bardziej skomplikowane, ponieważ osie inline i block są określane przez writing-mode elementu korzystającego z jednostki, a nie elementu, którego dotyczy zapytanie. Aby to umożliwić, polyfill stosuje styl w źródle do każdego elementu, którego writing-mode różni się od elementu nadrzędnego.

/* 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);

Teraz, tak jak wcześniej, jednostki można przekształcić w odpowiednią właściwość niestandardową CSS.

Właściwości

Zapytania dotyczące kontenera wprowadzają też kilka nowych właściwości CSS, takich jak container-typecontainer-name. Ponieważ interfejsów API, takich jak getComputedStyle(...), nie można używać z nieznanymi lub nieprawidłowymi właściwościami, są one również przekształcane w właściwości niestandardowe usługi porównywania cen po przeanalizowaniu. Jeśli nie można przeanalizować właściwości (np. dlatego, że zawiera ona nieprawidłową lub nieznaną wartość), pozostawia się ją do obsługi przez przeglądarkę.

Przed
.card {
  container-name: card-container;
  container-type: inline-size;
}
Po
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

Te właściwości są przekształcane, gdy tylko zostaną wykryte, co pozwala na prawidłowe działanie polyfilla z innymi funkcjami CSS, takimi jak @supports. Ta funkcja jest podstawą sprawdzonych metod korzystania z kodu polyfill, które opisaliśmy poniżej.

Przed
@supports (container-type: inline-size) {
  /* ... */
}
Po
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

Domyślnie właściwości niestandardowe CSS są dziedziczone, co oznacza, że np. każdy element podrzędny .card będzie miał wartość --cq-XYZ-container-name--cq-XYZ-container-type. Zdecydowanie tak nie działają usługi natywne. Aby rozwiązać ten problem, polyfill wstawia następującą regułę przed wszystkimi stylami użytkownika, aby każdy element otrzymał początkowe wartości, chyba że zostanie celowo zastąpiony przez inną regułę.

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

Sprawdzone metody

Chociaż większość użytkowników będzie prawdopodobnie korzystała z przeglądarek z wbudowanym mechanizmem obsługi zapytań kontenera, warto zadbać o to, aby pozostali użytkownicy również mieli dobre wrażenia.

Podczas początkowego wczytywania musi się wydarzyć wiele rzeczy, zanim polyfill będzie mógł utworzyć układ strony:

  • Rozszerzenie musi zostać załadowane i inicjowane.
  • Arkusze stylów muszą zostać przeanalizowane i przetłumaczone. Ponieważ nie ma interfejsów API, które umożliwiają dostęp do nieprzetworzonego źródła zewnętrznej spersonalizowanej czcionki, może być konieczne jej asynchroniczne ponowne pobieranie, ale najlepiej tylko z pamięci podręcznej przeglądarki.

Jeśli polyfill nie rozwiąże tych problemów, może to spowodować pogorszenie wyników podstawowych wskaźników internetowych.

Aby ułatwić Ci zapewnienie użytkownikom wygodnej obsługi, polyfill został zaprojektowany tak, aby nadawać priorytet opóźnieniu przy pierwszym działaniu (FID)skumulowanemu przesunięciu układu (CLS), nawet kosztem największego wyrenderowania treści (LCP). Konkretnie: kod polyfill nie gwarantuje, że zapytania dotyczące kontenerów zostaną ocenione przed pierwszym wyrenderowaniem. Oznacza to, że aby zapewnić użytkownikom jak najlepsze wrażenia, musisz zadbać o to, aby wszystkie treści, na które wpływają zapytania kontenera, były ukryte, dopóki nie załaduje się polyfill i nie przetłumaczy on Twojego kodu CSS. Możesz to zrobić na przykład za pomocą reguły @supports:

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

Zaleca się połączenie tego efektu z animacją wczytywania CSS umieszczoną całkowicie nad (ukrytą) treścią, aby poinformować użytkownika, że coś się dzieje. Pełne demo tego podejścia znajdziesz tutaj.

Takie podejście jest zalecane z kilku powodów:

  • Czysty moduł ładowania CSS minimalizuje obciążenie dla użytkowników korzystających z nowszych przeglądarek, zapewniając jednocześnie szybki feedback użytkownikom korzystającym ze starszych przeglądarek i wolniejszych sieci.
  • Połączenie bezwzględnego pozycjonowania wczytywania z wartością visibility: hidden zapobiega przesunięciu układu.
  • Po załadowaniu polyfillu warunek @supports przestanie być spełniony, a Twoje treści zostaną odsłonięte.
  • W przeglądarkach z wbudowanym wsparciem dla zapytań kontenera warunek nigdy nie zostanie spełniony, więc strona zostanie wyświetlona zgodnie z oczekiwaniami w ramach pierwszego wyrenderowania.

Podsumowanie

Jeśli chcesz używać zapytań kontenerowych w starszych przeglądarkach, wypróbuj polyfill. Jeśli napotkasz problemy, zgłoś je.

Nie możemy się doczekać, żeby zobaczyć i przeżyć niesamowite rzeczy, które zbudujesz.