Di dalam polyfill kueri container

Gerald Monaco
Gerald Monaco

Kueri penampung adalah fitur CSS baru yang memungkinkan Anda menulis logika gaya yang menargetkan fitur elemen induk (misalnya, lebar atau tingginya) untuk menata gaya turunannya. Baru-baru ini, pembaruan besar pada polyfill dirilis, yang bertepatan dengan dukungan yang tersedia di browser.

Dalam postingan ini, Anda akan dapat melihat cara kerja polyfill, tantangan yang diatasinya, dan praktik terbaik saat menggunakannya untuk memberikan pengalaman pengguna yang luar biasa bagi pengunjung.

Di balik layar

Transpilasi

Saat parser CSS di dalam browser menemukan aturan at yang tidak dikenal, seperti aturan @container yang baru, parser akan menghapusnya seolah-olah aturan tersebut tidak pernah ada. Oleh karena itu, hal pertama dan terpenting yang harus dilakukan polyfill adalah mentranspile kueri @container menjadi sesuatu yang tidak akan dihapus.

Langkah pertama dalam transpilasi adalah mengonversi aturan @container tingkat teratas menjadi kueri @media. Hal ini sebagian besar memastikan bahwa konten tetap dikelompokkan bersama. Misalnya, saat menggunakan API CSSOM dan saat melihat sumber CSS.

Sebelum
@container (width > 300px) {
  /* content */
}
Setelah
@media all {
  /* content */
}

Sebelum kueri penampung, CSS tidak memiliki cara bagi penulis untuk mengaktifkan atau menonaktifkan grup aturan secara arbitrer. Untuk melakukan polyfill pada perilaku ini, aturan di dalam kueri penampung juga perlu diubah. Setiap @container diberi ID unik sendiri (misalnya, 123), yang digunakan untuk mengubah setiap pemilih sehingga hanya akan berlaku jika elemen memiliki atribut cq-XYZ yang menyertakan ID ini. Atribut ini akan ditetapkan oleh polyfill saat runtime.

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

Perhatikan penggunaan class semu :where(...). Biasanya, menyertakan pemilih atribut tambahan akan meningkatkan spesifitas pemilih. Dengan class semu, kondisi tambahan dapat diterapkan sekaligus mempertahankan kekhususan aslinya. Untuk mengetahui alasan pentingnya hal ini, perhatikan contoh berikut:

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

.card {
  color: red;
}

Dengan CSS ini, elemen dengan class .card harus selalu memiliki color: red, karena aturan berikutnya akan selalu mengganti aturan sebelumnya dengan pemilih dan kekhususan yang sama. Oleh karena itu, mentranspile aturan pertama dan menyertakan pemilih atribut tambahan tanpa :where(...) akan meningkatkan spesifitas, dan menyebabkan color: blue diterapkan secara keliru.

Namun, pseudo-class :where(...) cukup baru. Untuk browser yang tidak mendukungnya, polyfill memberikan solusi yang aman dan mudah: Anda dapat secara sengaja meningkatkan kekhususan aturan dengan menambahkan pemilih :not(.container-query-polyfill) dummy secara manual ke aturan @container:

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

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

.card {
  color: red;
}

Hal ini memiliki sejumlah manfaat:

  • Pemilih di CSS sumber telah berubah, sehingga perbedaan dalam kekhususan terlihat secara eksplisit. Hal ini juga berfungsi sebagai dokumentasi sehingga Anda tahu apa yang terpengaruh saat Anda tidak perlu lagi mendukung solusi atau polyfill.
  • Spesifisitas aturan akan selalu sama, karena polyfill tidak mengubahnya.

Selama transpilasi, polyfill akan mengganti dummy ini dengan pemilih atribut dengan spesifitas yang sama. Untuk menghindari kejutan, polyfill menggunakan kedua pemilih: pemilih sumber asli digunakan untuk menentukan apakah elemen harus menerima atribut polyfill, dan pemilih yang ditranspil digunakan untuk menata gaya.

Elemen semu

Satu pertanyaan yang mungkin Anda tanyakan adalah: jika polyfill menetapkan beberapa atribut cq-XYZ pada elemen untuk menyertakan ID penampung unik 123, bagaimana pseudo-elemen, yang tidak dapat menetapkan atribut, dapat didukung?

Pseudo-elemen selalu terikat dengan elemen sebenarnya di DOM, yang disebut elemen asal. Selama transpilasi, pemilih bersyarat diterapkan ke elemen sebenarnya ini:

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

Alih-alih diubah menjadi #foo::before:where([cq-XYZ~="123"]) (yang akan tidak valid), pemilih bersyarat dipindahkan ke akhir elemen asal, #foo.

Namun, itu bukan satu-satunya hal yang diperlukan. Penampung tidak diizinkan untuk mengubah apa pun yang tidak terdapat di dalamnya (dan penampung tidak boleh berada di dalam dirinya sendiri), tetapi pertimbangkan bahwa hal itulah yang akan terjadi jika #foo adalah elemen penampung yang dikueri. Atribut #foo[cq-XYZ] akan salah diubah, dan aturan #foo apa pun akan salah diterapkan.

Untuk memperbaikinya, polyfill sebenarnya menggunakan dua atribut: satu yang hanya dapat diterapkan ke elemen oleh induk, dan satu yang dapat diterapkan elemen ke dirinya sendiri. Atribut terakhir digunakan untuk pemilih yang menargetkan pseudo-elemen.

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

Karena penampung tidak akan pernah menerapkan atribut pertama (cq-XYZ-A) ke dirinya sendiri, pemilih pertama hanya akan cocok jika penampung induk yang berbeda telah memenuhi kondisi penampung dan menerapkannya.

Unit relatif penampung

Kueri penampung juga dilengkapi dengan beberapa unit baru yang dapat Anda gunakan di CSS, seperti cqw dan cqh untuk 1% lebar dan tinggi (masing-masing) dari penampung induk terdekat yang sesuai. Untuk mendukung hal ini, unit diubah menjadi ekspresi calc(...) menggunakan Properti Khusus CSS. Polyfill akan menetapkan nilai untuk properti ini melalui gaya inline pada elemen penampung.

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

Ada juga unit logis, seperti cqi dan cqb untuk ukuran inline dan ukuran blok (masing-masing). Hal ini sedikit lebih rumit, karena sumbu inline dan blok ditentukan oleh writing-mode dari elemen yang menggunakan unit, bukan elemen yang dikueri. Untuk mendukung hal ini, polyfill menerapkan gaya inline ke elemen apa pun yang writing-mode-nya berbeda dengan induknya.

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

Sekarang, unit dapat diubah menjadi Properti Khusus CSS yang sesuai seperti sebelumnya.

Properti

Kueri penampung juga menambahkan beberapa properti CSS baru seperti container-type dan container-name. Karena API seperti getComputedStyle(...) tidak dapat digunakan dengan properti yang tidak diketahui atau tidak valid, API ini juga diubah menjadi Properti Khusus CSS setelah diuraikan. Jika properti tidak dapat diuraikan (misalnya, karena berisi nilai yang tidak valid atau tidak diketahui), properti tersebut akan dibiarkan saja untuk ditangani oleh browser.

Sebelum
.card {
  container-name: card-container;
  container-type: inline-size;
}
Setelah
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

Properti ini diubah setiap kali ditemukan, sehingga polyfill dapat berfungsi dengan baik dengan fitur CSS lainnya seperti @supports. Fungsi ini adalah dasar dari praktik terbaik untuk menggunakan polyfill, seperti yang dibahas di bawah.

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

Secara default, Properti Kustom CSS diwariskan, yang berarti, misalnya, setiap turunan .card akan menggunakan nilai --cq-XYZ-container-name dan --cq-XYZ-container-type. Itu jelas bukan cara kerja properti native. Untuk mengatasi hal ini, polyfill akan menyisipkan aturan berikut sebelum gaya pengguna, memastikan bahwa setiap elemen menerima nilai awal, kecuali jika sengaja diganti oleh aturan lain.

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

Praktik terbaik

Meskipun diperkirakan sebagian besar pengunjung akan menjalankan browser dengan dukungan kueri penampung bawaan dalam waktu dekat, Anda tetap harus memberikan pengalaman yang baik kepada pengunjung yang tersisa.

Selama pemuatan awal, ada banyak hal yang perlu dilakukan sebelum polyfill dapat menata letak halaman:

  • Polyfill perlu dimuat dan diinisialisasi.
  • Stylesheet perlu diuraikan dan ditranspile. Karena tidak ada API untuk mengakses sumber mentah stylesheet eksternal, stylesheet mungkin perlu diambil ulang secara asinkron, meskipun idealnya hanya dari cache browser.

Jika masalah ini tidak ditangani dengan cermat oleh polyfill, hal ini berpotensi menyebabkan regresi Data Web Inti Anda.

Untuk memudahkan Anda memberikan pengalaman yang menyenangkan kepada pengunjung, polyfill dirancang untuk memprioritaskan Penundaan Input Pertama (FID) dan Pergeseran Tata Letak Kumulatif (CLS), yang berpotensi mengorbankan Largest Contentful Paint (LCP). Secara konkret, polyfill tidak menjamin bahwa kueri penampung Anda akan dievaluasi sebelum proses rendering pertama. Artinya, untuk pengalaman pengguna terbaik, Anda harus memastikan bahwa konten yang ukuran atau posisinya akan terpengaruh dengan menggunakan kueri penampung disembunyikan hingga setelah polyfill memuat dan mentranspile CSS Anda. Salah satu cara untuk melakukannya adalah dengan menggunakan aturan @supports:

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

Sebaiknya gabungkan ini dengan animasi pemuatan CSS murni, yang diposisikan secara mutlak di atas konten (tersembunyi) Anda, untuk memberi tahu pengunjung bahwa ada sesuatu yang terjadi. Anda dapat menemukan demo lengkap pendekatan ini di sini.

Pendekatan ini direkomendasikan karena sejumlah alasan:

  • Loader CSS murni meminimalkan overhead untuk pengguna dengan browser yang lebih baru, sekaligus memberikan masukan ringan kepada pengguna di browser lama dan jaringan yang lebih lambat.
  • Dengan menggabungkan pemosisian absolut loader dengan visibility: hidden, Anda akan menghindari pergeseran tata letak.
  • Setelah polyfill dimuat, kondisi @supports ini akan berhenti diteruskan, dan konten Anda akan ditampilkan.
  • Di browser dengan dukungan bawaan untuk kueri penampung, kondisi tersebut tidak akan pernah lulus, sehingga halaman akan ditampilkan pada proses rendering pertama seperti yang diharapkan.

Kesimpulan

Jika Anda tertarik untuk menggunakan kueri penampung di browser lama, coba polyfill. Jangan ragu untuk melaporkan masalah jika Anda mengalami masalah.

Kami tidak sabar untuk melihat dan merasakan hal-hal luar biasa yang akan Anda buat dengan teknologi ini.