Dowiedz się, jak używać zasięgu @, aby wybierać elementy tylko w ograniczonym poddrzewie DOM.
Delikatna sztuka tworzenia selektorów arkusza CSS
Podczas pisania selektorów możesz czuć się rozdarta między 2 światami. Z jednej strony musisz dokładnie określić, które elementy chcesz wybrać. Z drugiej strony chcesz, aby selektory były łatwe do zastąpienia i nie były ściśle powiązane ze strukturą DOM.
Jeśli na przykład chcesz wybrać „obraz nagłówka w obszarze treści komponentu karty”, co jest dość specyficznym wyborem elementu, prawdopodobnie nie chcesz pisać selektora takiego jak .card > .content > img.hero.
- Ten selektor ma dość wysoką specyficzność (0,3,1), co utrudnia jego zastąpienie w miarę rozrastania się kodu.
- Korzystając z kombinatora bezpośredniego podrzędnego, jest ściśle powiązany ze strukturą DOM. Jeśli znaczniki ulegną zmianie, musisz też zmienić plik CSS.
Nie chcesz jednak wpisywać jako selektora tego elementu tylko img, ponieważ spowodowałoby to wybranie wszystkich elementów obrazu na stronie.
Znalezienie odpowiedniej równowagi jest często dość trudne. W ciągu lat niektórzy deweloperzy opracowali rozwiązania i metody obejścia problemów, które mogą Ci pomóc w takich sytuacjach. Na przykład:
- Metodologie takie jak BEM wymagają, aby ten element miał klasę card__img card__img--hero, co pozwala zachować niską specyficzność, a jednocześnie pozwala na dokładne określenie tego, co wybierasz.
- Rozwiązania oparte na JavaScript, takie jak CSS ograniczony czy składniki stylizowane, przepisują wszystkie selektory, dodając do nich losowo wygenerowane ciągi znaków, np. sc-596d7e0e-4, aby zapobiec ich kierowaniu na elementy po drugiej stronie strony.
- Niektóre biblioteki całkowicie rezygnują z selektorów i wymagają umieszczania elementów sterujących stylami bezpośrednio w tagach.
Co jednak, jeśli nie potrzebujesz żadnego z tych elementów? Co, jeśli CSS dałoby Ci możliwość precyzyjnego wybierania elementów bez konieczności pisania selektorów o wysokiej specyficzności lub takich, które są ściśle powiązane z Twoim DOM-em? Właśnie w tym przypadku przydaje się funkcja @scope, która umożliwia wybieranie elementów tylko w poddrzewie DOM.
Przedstawiamy @scope
Za pomocą @scope możesz ograniczyć zasięg selektorów. Aby to zrobić, ustaw korzeń zakresu, który określa górną granicę poddrzewa, na które chcesz kierować reklamy. Gdy ustawisz ograniczający element główny, zawarte w nim reguły stylów (nazywane ograniczonymi regułami stylów) mogą wybierać tylko z ograniczonego poddrzewa DOM.
Jeśli na przykład chcesz kierować się tylko na elementy <img> w komponencie .card, ustawiasz .card jako element nadrzędny reguły at-rule @scope.
@scope (.card) {
    img {
        border-color: green;
    }
}
Reguła stylu ograniczonego zakresem img { … } może skutecznie wybierać tylko elementy <img>, które są w zakresie dopasowanego elementu .card.
Aby zapobiec zaznaczaniu elementów <img> w obszarze treści karty (.card__content), możesz bardziej sprecyzować selektor img. Innym sposobem jest wykorzystanie faktu, że reguła @scope przyjmuje też ograniczenie zakresu, które określa dolną granicę.
@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}
Ta ograniczona reguła stylu dotyczy tylko elementów <img>, które w drzewie przodków znajdują się między elementami .card i .card__content. Ten typ zakresu z górną i dolną granicą często nazywany jest zakresem ciastka z lukrem.
Selektor :scope
Domyślnie wszystkie reguły stylu z ograniczeniami są względne do katalogu źródeł. Możesz też kierować reklamy na sam element skojarzony z elementem głównym. Użyj do tego selektora :scope.
@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}
Selektory w ramach reguł stylów ograniczonych są domyślnie poprzedzane przez :scope. Jeśli chcesz, możesz wyraźnie to zaznaczyć, dodając przedrostek :scope. Możesz też użyć selektora & z zagnieżdżania CSS.
@scope (.card) {
    img {
       /* Selects img elements that are a child of .card */
    }
    :scope img {
        /* Also selects img elements that are a child of .card */
    }
    & img {
        /* Also selects img elements that are a child of .card */
    }
}
Limit zakresu może używać pseudoklasy :scope, aby wymagać określonego związku z korzeniami zakresu:
/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }
Limit zakresu może się też odwoływać do elementów spoza swojego elementu skojarzonego za pomocą elementu :scope. Na przykład:
/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }
Pamiętaj, że reguły stylów ograniczonych nie mogą wykraczać poza poddrzewo. Wybrane elementy, takie jak :scope + p, są nieprawidłowe, ponieważ próbują wybrać elementy, które nie są objęte zakresem.
@scope i specyficzność
Selektory użyte w preludium do @scope nie wpływają na specyficzność zawartych selektorów. W przykładzie poniżej specyficzność selektora img nadal wynosi (0,0,1).
@scope (#sidebar) {
    img { /* Specificity = (0,0,1) */
        …
    }
}
Specyficzność :scope jest taka jak w przypadku zwykłej pseudoklasy, czyli (0,1,0).
@scope (#sidebar) {
    :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
        …
    }
}
W tym przykładzie wewnętrznie selektor & jest zastępowany przez selektor używany do określania zakresu, który jest otoczony selektorem :is(). W efekcie przeglądarka użyje selektora :is(#sidebar, .card) img do dopasowania. Ten proces nazywa się usuwaniem cukru.
@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        …
    }
}
Ponieważ & jest odsłodzony za pomocą :is(), specyficzność & jest obliczana zgodnie z zasadami specyficzności :is(): specyficzność & jest taka jak najbardziej szczegółowego argumentu.
W tym przykładzie szczegółowość argumentu :is(#sidebar, .card) jest taka sama jak szczegółowość najbardziej szczegółowego argumentu, czyli #sidebar, i w konsekwencji staje się (1,0,0). Połącz to ze specyfiką img, która wynosi (0,0,1), a w efekcie specyfika dla całego złożonego selektora to (1,0,1).
@scope (#sidebar, .card) {
    & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
        …
    }
}
Różnica między :scope a & w wersji @scope
Oprócz różnic w sposobie obliczania specyficzności :scope i & różnią się tym, że :scope reprezentuje dopasowany korzeń zakresu, a & – selektor użyty do dopasowania do korzenia zakresu.
Z tego powodu można użyć & kilka razy. W przeciwieństwie do :scope, którego można użyć tylko raz, ponieważ nie można dopasować korzenia zakresu w korzenia zakresu.
@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :scope :scope { /* ❌ Does not work */
    …
  }
}
Zakres bez wstępu
Podczas pisania stylów wbudowanych za pomocą elementu <style> możesz ograniczyć zakres reguł stylów do elementu nadrzędnego elementu <style>, nie określając żadnego korzenia zakresu. Aby to zrobić, pomiń wstęp @scope.
<div class="card">
  <div class="card__header">
    <style>
      @scope {
        img {
          border-color: green;
        }
      }
    </style>
    <h1>Card Title</h1>
    <img src="…" height="32" class="hero">
  </div>
  <div class="card__content">
    <p><img src="…" height="32"></p>
  </div>
</div>
W przykładzie powyżej reguły ograniczone dotyczą tylko elementów w elementach div o nazwie klasy card__header, ponieważ element div jest elementem nadrzędnym elementu <style>.
@scope w kaskadzie
W kaskadzie CSS @scope dodaje też nowe kryterium: bliskość zakresu. Ten krok pojawia się po specyficzności, ale przed kolejnością występowania.
Porównując deklaracje, które pojawiają się w regułach stylów z różnymi korzeniami zakresu, wygrywa deklaracja z najmniejszą liczbą skoków między korzeniami generacji lub elementami siostrzanymi a podmiotem reguły stylu z zakresem.
Ten nowy krok jest przydatny podczas zagnieżdżania kilku wariantów komponentu. Weź pod uwagę ten przykład, w którym nie użyto jeszcze znacznika @scope:
<style>
    .light { background: #ccc; }
    .dark  { background: #333; }
    .light a { color: black; }
    .dark a { color: white; }
</style>
<div class="light">
    <p><a href="#">What color am I?</a></p>
    <div class="dark">
        <p><a href="#">What about me?</a></p>
        <div class="light">
            <p><a href="#">Am I the same as the first?</a></p>
        </div>
    </div>
</div>
Podczas wyświetlania tego fragmentu znacznika trzeci link będzie miał wartość white zamiast black, mimo że jest elementem podrzędnym elementu div z zastosowanej klasą .light. Wynika to z kryterium kolejności wyświetlania, którego używa kaskada do określenia zwycięzcy. Widzi, że .dark a zostało zadeklarowane jako ostatnie, więc wygra na podstawie reguły .light a
Dzięki kryterium zbliżenia do zakresu problem został rozwiązany:
@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}
@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}
Ponieważ oba ograniczone selektory a mają tę samą specyficzność, w działanie wchodzi kryterium ograniczania zasięgu. Oblicza wagę obu selektorów na podstawie ich bliskości do korzenia zakresu. W przypadku tego trzeciego elementu a do głównego elementu .light jest tylko 1 skoczek, a do elementu .dark – 2 skoczki. Dlatego zwycięży selektor a w elementach .light.
Uwaga końcowa: izolacja selektora, a nie izolacja stylu
Pamiętaj, że @scope ogranicza zasięg selektorów, ale nie zapewnia izolacji stylów. Właściwości, które są dziedziczone przez elementy podrzędne, będą nadal dziedziczone, nawet jeśli wartość @scope jest większa niż dolna granica. Jedną z takich usług jest color. Gdy zadeklarujesz to w zakresie donut, color będzie nadal dziedziczyć wartości od elementów wewnątrz otworu donut.
@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}
W powyższym przykładzie element .card__content i jego elementy podrzędne mają kolor hotpink, ponieważ dziedziczą tę wartość z elementu .card.
(Zdjęcie na okładce: rustam burkhanov na Unsplash)
