Damian Słoński

CSS Nesting natywnie w przeglądarce 👨‍👩‍👧‍👦

Dlaczego coś znanego developerom w zasadzie od lat, może być zwiastunem przełomowych zmian w technologiach webowych?

  • web
  • performance
CSS Nesting natywnie w przeglądarce 👨‍👩‍👧‍👦

Wstęp ⚙️

W dniu 29.07.2023 Firefox w wersji 117 wprowadził wparcie dla natywnego zagdnieżdżania selektorów w CSS, wypełniając tym brakującą lukę obok Chrome i Safari. Czy zatem możemy już zacząć mówić o początku końca ery preprocesorów?

Z powodu ograniczeń w języku CSS powstały tzn. preprocesory, które pozwalają na pisanie kodu w składni przypominającym język CSS (wprowadzając dodatkowe możliwości), a następnie jego kompilację go do zwykłego CSS-a, który jest interpretowany przez przeglądarkę, w przeciwieństwie do kodu źródłowego pisanego w składni danego preprocesora.

1// source code in Scss:
2$main-color: #220000; // Scss variable
3
4.mybutton {
5 color: $main-color;
6}
7
8// output for the browser in CSS:
9.mybutton {
10 color: #220000;
11}

Zagnieżdzanie selektorów w preprocesorach dostępne było już od dawna, podobnie jak w technologiach typu CSS-in-JS, jednak po raz pierwszy staje się to dostępne w CSS-ie natywnie, co za tym idzie - bezpośrednio w przeglądarkach.

Składnia zagnieżdżania selektorów 📝

Składnia zagnieżdżania w CSS jest analogiczna jak w preprocesorach takich jak Scss. Zamiast używać zapisu:

1.navigation {
2 color: #111;
3}
4
5.navigation .link {
6 font-size: 18px;
7}

Możemy użyć składni:

1.navigation {
2 color: #111;
3
4 .link {
5 font-size: 14px;
6 }
7}

Oczywiście elementy możemy zagnieżdżać wielokrotnie. Przkład:

1.navigation {
2 color: #111;
3
4 /* .navigation .link */
5 .link {
6 font-size: 14px;
7
8 /* .navigation .link:hover */
9 &:hover {
10 text-decoration: underline;
11 }
12
13 /* .navigation .link.social */
14 &.social {
15 border-radius: 50%;
16
17 /* .navigation .link.social:hover */
18 &:hover {
19 text-decoration: none;
20 }
21 }
22 }
23}

Również bezproblemowo działa zagnieżdżanie media queries czy container queries. Przykład:

1.navigation {
2 display: flex;
3 flex-direction: column;
4
5 @media (min-width: 1024px) {
6 flex-direction: row;
7 }
8}

Wsparcie przeglądarek 🌐

Wsparcie dla CSS nesting można dynamicznie sprawdzać za pomocą reguły @supports w CSS lub za pomocą querySelector w JS-ie, opierając się na dostępności selectora & :

1@supports (selector(&)) {
2 /* ... */
3}
1let isCSSNestingSupported = false
2try {
3 isCSSNestingSupported = Boolean(document.querySelector("&"))
4} catch (err) {
5 console.error(err) // throw: DOMException: Document.querySelector: '&' is not a valid selector
6}

Za pomocą komponentu poniżej, możesz sprawdzić czy używana przez Ciebie przeglądarka wspiera już zagnieżdżanie selektorów:

1output {
2 /* ... */
3
4 @media (min-width: 1024px) {
5 font-style: italic;
6 }
7
8 & strong {
9 color: green;
10 }
11}
Lorem ipsum dolor sit amet consectetur adipisicing elit. Veniam cum numquam explicabo eius nesciunt quo voluptates? Text with green color.

❌ Twoje przeglądarka nie wspiera jeszcze tej funkcjonalności!
ℹ️ Sprawdź wsparcie: caniuse.com/css-nesting

Różnice względem dziedziczenia w preprocesorach 🤔

Jest kilka różnić, które warto mieć na uwadze. Pierwsza istotna różnica polega na tym, że jeśli selector nie zaczyna się od jednego z tych znaków: & @ : . > ~ + # [ * (np. article, p img czy każdy inny tzw. Type selector) to selektor ten należy poprzedzić znakiem &:

1/* ✅ will work in Scss */
2article {
3 h2 {
4 font-size: 18px;
5 }
6}
1/* ❌ won't work in CSS */
2article {
3 h2 {
4 font-size: 18px;
5 }
6}
7
8/* ✅ will work in CSS */
9article {
10 & h2 {
11 font-size: 18px;
12 }
13}

Wynika to z faktu, że mocno obciążyło by to przeglądarkę, gdyby za każdym razem musiała rozwiązywać problem polegający na tym, aby rozróżnić, czy ma do czynienia z selektorem czy z właściwością CSS. Przykład:

1.canvas {
2 font: 14px bold;
3
4 & font:hover {
5 text-decoration: underline;
6 }
7
8 /* font:hover { ... } */
9 /* 👆 "font" could be a property with incorect value */
10 /* but also a selector for the <font> tag */
11}

Jednak istnieje spore prawdopodonieństwo, że w kolejnych wersjach ograniczenie to zostanie wyeliminowane.

Druga istotna różnica to brak możliwości stosowania konkatenacji z selektorem rodzica, który jest np. bardzo często wykorzystywany na potrzeby metodologii BEM. Przykład:

1/* ✅ will work in Scss */
2.navigation {
3 &__link {
4 font-size: 14px;
5 }
6}
1/* ❌ won't work in CSS */
2.navigation {
3 &__link {
4 font-size: 14px;
5 }
6}
7
8/* ✅ will work in CSS */
9.navigation {
10 .navigation__link {
11 font-size: 14px;
12 }
13}

Kod Scss jest kompilowany i w trakcie kompilacji zapis &__link zamieniany jest na .navigation__link. W natwynym CSS-ie nie ma takiej możliwości, więc musimy użyć pełnej nazwy selektora rodzica.

Warto również wiedzieć, że specyficzność selektorów może się różnić przy takim samym zapisie w przypadku Scss oraz CSS. Przykład:

1article,
2#contact-section {
3 p {
4 font-size: 12px;
5 }
6}
7
8// compiled to:
9// 0-0-2 for first one
10// 1-0-1 for second one
11article p,
12#contact-section p {
13 font-size: 12px;
14}
1article,
2#contact-section {
3 p {
4 font-size: 12px;
5 }
6}
7
8/* for browsers is like: */
9/* 1-0-1 for both */
10:is(article, #contact-section) p {
11 font-size: 12px;
12}

Dlaczego tak się dzieje? Po wyjaśnienie specyficzności pseudoselektora :is() odsyłam do MDN docs.

Wskazówki dotyczące zagnieżdżania selektorów 💡

Zbyt głębokie selektory

Warto uważać na zbyt złożone selektory, szczególnie łatwo się na tym złapać podczas wykorzystywania zagnieżdżania selektorów. Wpływa to po pierwsze na czytelność kodu, ale co ważniejsze negatywnie odbija się również na wydajności parsowania stylów przez przeglądarkę podczas fazy Style computation w procesie renderowania widoku. Warto ograniczać się do 3, 4 poziomów (zakładając wyjątki w razie potrzeby), jednak w pierwszej kolejności warto rozważyć dodanie abstrakcji w postaci klasy, w celu uproszczenia kodu.

1/* ❌ try avoid this: */
2body {
3 main {
4 .container {
5 & article {
6 & button {
7 & strong {
8 color: #222;
9 }
10 }
11 }
12 }
13 }
14}
15
16/* ✅ prefer this: */
17.primary-button {
18 & strong {
19 color: #222;
20 }
21}
1<body>
2 <main>
3 <div class="container">
4 <article>
5 <!-- consider add class for cases like this 👇 -->
6 <button class="primary-button">
7 Read more<strong>(5 min)</strong>
8 </button>
9 </article>
10 </div>
11 </main>
12</body>

Chaotyczna kolejność

O ile nie nadpisujemy wartości, to kolejność selektorów nie ma większego znaczenia, jednak warto przyjąć i starać się przestrzegać wybranej przez siebie konwencji. Ma to znaczenie zwłaszcza w większych projektach, które posiadają wiele plików ze stylami, nad którymi pracuje wielu programistów. Proponowana prze ze mnie kolejność:

  1. Właściwości dla obecnego selektora
  2. Zagnieżdżone elementy
  3. Zagnieżdżone media queries w kolejności mobile first
1/* ❌ try avoid this: */
2.card {
3 @media (min-width: 1024px) {
4 /* ... */
5 }
6
7 .btn {
8 /* ... */
9 }
10
11 @media (min-width: 480px) {
12 /* ... */
13 }
14
15 border-radius: 4px;
16
17 .title {
18 /* ... */
19 }
20
21 .btn:hover {
22 /* */
23 }
24}
25
26/* ✅ prefer this: */
27.card {
28 border-radius: 4px;
29
30 .title {
31 /* ... */
32 }
33
34 .btn {
35 /* ... */
36
37 &:hover {
38 /* ... */
39 }
40 }
41
42 @media (min-width: 480px) {
43 /* ... */
44 }
45
46 @media (min-width: 1024px) {
47 /* ... */
48 }
49}

Czy technologie webowe zataczają koło? 🔄

10 lat temu podstawowy wachlarz technologii webowych (HTML, CSS i JS) i ich ówczesne możliwości były zbyt prymitywne jak na wyzwania, które stały przed aplikacjami webowymi. Z tego powodu powstało wiele technologii, które:

  1. Rozszerzały możliwości tych języków, nadbudowując je (np. Sass, TypeScript).
  2. Pozwałały skompilować całość do języka zrozumiałego przez przeglądarkę (np. Webpack, Babel).

Ogromne możliwości, które otworzyły się przed developerami sprawiły, że łatwo było się tymi możliwościami zachłysnąć. Dobór zbyt wielu technologi, skomplikowany proces kompilacji oparty na wielu narzędziach i konfiguracjach, które były trudne do zrozumienia, niejednokrotnie sprawiały, że utrzymywanie takich aplikacji stawało się bardzo problematyczne, a próg wejścia dla nowych osób był bardzo wysoki.

Jednak od tamtego czasu języki natywnie dostępne w przeglądarce zaliczyły spory progres. Natywnie dostępne zagnieżdżanie selektorów w czystym CSS jest jednym z wielu przykładów, obok CSS Custom Properties albo ESModules czy WebComponents dla JS-a. Do tego w ostatnim czasie na popularności zyskują też technologie, które niejako próbują łączyć i wykorzystać korzyści obu tych światów (Tailwind CSS czy Vite).

Tworzenie stylów dla większych aplikacji bez Scss albo CSS-in-JS, podobnie jak organizowanie logiki kodu bez TypeScript-a czy Webpacka, na chwilę obecną wydaje się być wręcz niemożliwe. Mam jednak wrażenie, że odległość pomiędzy szeroko rozumianymi technologiami opartymi na kompilacji a technologiami natywnymi z każdym rokiem się zmniejsza. Czy zatoczymy koło i w przyszłości będziemy znów pisać aplikacje webowe bez użycia preprocesorów i kompilatorów? Czas pokaże.