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ę kodu polyfill, która zbiegła się z wprowadzeniem obsługi w przeglądarkach.

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

Dla zaawansowanych

Transpilacja

Gdy parser CSS w przeglądarce natrafi na nieznaną regułę „ad”, np. nową regułę @container, odrzuca ją, jakby nigdy nie istniała. Pierwszą i najważniejszą rzeczą, jaką musi zrobić polyfill, jest transpilacja zapytania @container w element, który nie zostanie odrzucony.

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 zapytaniami o kontenery CSS nie umożliwiał autorom dowolnie włączania i wyłączania grup reguł. Aby możliwe było polyfill, należy również przekształcić reguły wewnątrz zapytania kontenera. 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 jest ustawiany przez kod polyfill w czasie działania.

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

Zwróć uwagę na użycie pseudoklasy :where(...). Zwykle uwzględnienie dodatkowego selektora atrybutów zwiększa specyficzność selektora. W przypadku pseudoklasy można zastosować dodatkowy warunek, zachowując oryginalność. Aby przekonać się, dlaczego to takie ważne, zapoznaj się z tym przykładem:

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

.card {
  color: red;
}

Z uwzględnieniem tego kodu CSS element z klasą .card powinien zawsze mieć wartość color: red, ponieważ późniejsza reguła będzie zawsze zastępować poprzednią regułę z tym samym selektorem i konkretnością. Transpilacja pierwszej reguły i dodanie dodatkowego selektora atrybutu bez atrybutu :where(...) w związku z tym zwiększy szczegółowość i spowoduje nieprawidłowe zastosowanie wartości color: blue.

Jednak pseudoklasa :where(...) jest dość nowa. W przypadku przeglądarek, które nie obsługują kodu polyfill, można to bezpiecznie i łatwo obejść: możesz celowo zwiększyć precyzję reguł, ręcznie dodając do reguł @container fikcyjny selektor :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;
}

Ma to kilka zalet:

  • Selektor w źródłowym kodzie CSS zmienił się, więc różnica w szczegółowości jest wyraźnie widoczna. Jest to również dokumentacja informująca, na czym polega problem, gdy nie trzeba już obsługiwać obejścia lub kodu polyfill.
  • Szczegółowość reguł zawsze będzie taka sama, ponieważ kod polyfill nie zmienia jej.

Podczas transpilacji kod polyfill zastąpi ten szablon selektorem atrybutów z taką samą szczegółowoś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.

Pseudoelementy

Być może zadajesz sobie pytanie: jeśli kod polyfill ustawia atrybut cq-XYZ w elemencie tak, że zawiera on unikalny identyfikator kontenera 123, w jaki sposób 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 do tego rzeczywistego elementu jest stosowany selektor warunkowy:

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

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

Jednak to nie wszystko. Kontener nie może modyfikować niczego, co nie znajduje się w nim (a kontener nie może być wewnątrz niego), ale weź pod uwagę to, co by się stało, gdyby element #foo byłby przedmiotem zapytania. Atrybut #foo[cq-XYZ] zostałby błędnie zmieniony, a reguły #foo zostałyby 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 je obsługiwać, jednostka jest przekształcana w wyrażenie calc(...) za pomocą właściwości niestandardowych 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, np. cqi i cqb określające rozmiar wbudowanego tekstu i rozmiar bloku. Są one nieco bardziej skomplikowane, ponieważ osie wbudowane i blokowe są określane przez parametr writing-mode elementu używającego jednostki, a nie od elementu, którego dotyczy zapytanie. W tym celu kod polyfill stosuje styl wbudowany do każdego elementu, którego atrybut 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);

Jednostki można teraz przekształcić w odpowiednie właściwości niestandardowe CSS, tak jak wcześniej.

Właściwości

Zapytania dotyczące kontenerów dodają też kilka nowych właściwości CSS, takich jak container-type i container-name. Interfejsów API takich jak getComputedStyle(...) nie można używać z nieznanymi lub nieprawidłowymi właściwościami, dlatego po analizie są one również przekształcane w niestandardowe właściwości CSS. Jeśli właściwości nie można przeanalizować (na przykład zawiera ona nieprawidłową lub nieznaną wartość), pozostaje ona bezczynna i obsługuje ją przeglądarka.

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 po wykryciu, dzięki czemu kod polyfill dobrze komponuje się 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 na przykład każdy element podrzędny parametru .card przyjmie wartości --cq-XYZ-container-name i --cq-XYZ-container-type. Zdecydowanie tak nie działają usługi natywne. Aby rozwiązać ten problem, kod polyfill wstawia tę regułę przed stylami użytkownika. Dzięki temu każdy element otrzymuje wartości początkowe, chyba że celowo zastąpi ją inna reguła.

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

Sprawdzone metody

Spodziewa się, że większość użytkowników w najbliższym czasie uruchomi przeglądarki z wbudowaną obsługą zapytań w kontenerze, jednak nadal należy zadbać o dobre wrażenia pozostałych użytkowników.

Podczas wczytywania początkowego musi się zdarzyć, że kod polyfill będzie mógł określić układ strony:

  • Plik polyfill musi zostać załadowany i zainicjowany.
  • Arkusze stylów muszą zostać przeanalizowane i transpilowane. Ponieważ nie istnieją żadne interfejsy API umożliwiające dostęp do nieprzetworzonego źródła zewnętrznego arkusza stylów, może być konieczne ponowne pobranie go asynchronicznie, najlepiej jednak tylko z pamięci podręcznej przeglądarki.

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

Aby ułatwić Ci zapewnienie użytkownikom wygody korzystania z elementu polyfill, zaprojektowaliśmy go tak, aby priorytetowo traktował opóźnienie po pierwszym działaniu (FID) i skumulowane przesunięcie układu (CLS), potencjalnie kosztem największego wyrenderowania treści (LCP). Konkretnie: kod polyfill nie gwarantuje, że zapytania dotyczące kontenera zostaną ocenione przed pierwszym wyrenderowaniem. Oznacza to, że aby zadbać o wygodę użytkowników, musisz zadbać o to, aby wszelkie treści, na których rozmiar lub pozycja mogłyby mieć wpływ na użycie zapytań o kontener, były ukryte do czasu, aż kod polyfill załaduje się i przetranspiluje kod 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łną demonstrację tego podejścia znajdziesz tutaj.

Jest to zalecane z kilku powodów:

  • Wczytywanie czystego kodu CSS minimalizuje nakład pracy w przypadku użytkowników nowszych przeglądarek, a jednocześnie przekazuje mniej informacji w przypadku użytkowników starszych przeglądarek i wolniejszych sieci.
  • Łącząc bezwzględne pozycjonowanie modułu wczytywania z parametrem visibility: hidden, unikasz przesunięcia układu.
  • Po wczytaniu kodu polyfill ten warunek @supports przestanie być spełniony, a treści zostaną ujawnione.
  • W przeglądarkach z wbudowaną obsługą zapytań dotyczących kontenerów warunek nigdy nie zostanie spełniony, więc strona wyświetli się przy pierwszym wyrenderowaniu zgodnie z oczekiwaniami.

Podsumowanie

Jeśli chcesz korzystać z zapytań dotyczących kontenerów w starszych przeglądarkach, wypróbuj funkcję polyfill. Jeśli natrafisz na jakiś problem, zgłoś go.

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

Podziękowania

Baner powitalny: Dan Cristian Pădureț na kanale Unsplash.