Ogranicz zasięg selektorów za pomocą reguły @scope w CSS

Dowiedz się, jak używać @scope do wybierania elementów tylko w ograniczonym poddrzewie DOM.

Obsługa przeglądarek

  • 118
  • 118
  • x
  • x

Delikatna sztuka pisania selektorów CSS

Podczas pisania selektorów może się zdarzyć, że rozerwisz się między dwoma światami. Z pierwszej strony warto dokładnie określić, które elementy wybierasz. 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 np. chcesz wybrać „Baner powitalny w obszarze treści karty” – czyli dość konkretny wybór elementu – prawdopodobnie nie warto zapisywać selektora typu .card > .content > img.hero.

  • Ten selektor ma dość wysoką specyficzność wynoszącą (0,3,1), co utrudnia zastąpienie w miarę rozwoju kodu.
  • Dzięki bezpośredniemu kombinatorowi podrzędnemu jest on ściśle powiązany ze strukturą DOM. Jeśli znaczniki się zmienią, konieczna będzie zmiana także kodu CSS.

Nie chcesz też jednak wpisywać jako selektora tego elementu tylko img, ponieważ spowoduje to wybranie wszystkich elementów graficznych na stronie.

Znalezienie właściwej równowagi w tym zakresie jest często nie lada wyzwaniem. Na przestrzeni lat niektórzy deweloperzy wypracowali rozwiązania i obejścia, które mogą pomóc w takich sytuacjach. Na przykład:

  • Zgodnie z metodologią BEM należy nadać temu elementowi klasę card__img card__img--hero, aby zachować specyfikację na małą skalę, a jednocześnie precyzyjnie określić to, co wybierasz.
  • Rozwiązania oparte na języku JavaScript, np. Zakres CSS czy Komponenty ze stylem, przepisują wszystkie selektory, dodając do nich generowane losowo ciągi, np. sc-596d7e0e-4, aby uniemożliwić im kierowanie elementów po drugiej stronie strony.
  • Niektóre biblioteki całkowicie znoszą selektory i wymagają umieszczenia reguł określających styl bezpośrednio w znacznikach.

A co, jeśli nie są potrzebne? A co, jeśli dzięki CSS można bardzo precyzyjnie określać, które elementy mają zostać wybrane, bez konieczności pisania selektorów o dużej precyzji lub ściśle powiązanych z modelem DOM? Właśnie tu wkracza do gry @scope, który umożliwia wybieranie elementów tylko w poddrzewie DOM.

Przedstawiamy @scope

Opcja @scope pozwala ograniczyć zasięg selektorów. Aby to zrobić, ustaw pierwiastek zakresu, który określa górną granicę drzewa podrzędnego, na które chcesz kierować reklamy. W przypadku zbioru głównego zakresu zawartego w nim reguły stylu – o nazwie reguły stylu ograniczonego – mogą wybierać tylko z tego ograniczonego drzewa podrzędnego DOM.

Aby na przykład kierować reklamy tylko na elementy <img> w komponencie .card, ustaw .card jako pierwiastek zakresu w regule @scope.

@scope (.card) {
    img {
        border-color: green;
    }
}

Reguła stylu zakresu img { … } może efektywnie wybierać tylko elementy <img> wchodzące w zakres dopasowanego elementu .card.

Aby uniemożliwić zaznaczenie elementów <img> w obszarze treści karty (.card__content), możesz doprecyzować selektor img. Innym sposobem jest skorzystanie z faktu, że reguła @scope akceptuje też limit zakresu, który określa dolną granicę.

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

Ta reguła stylu zakresu obejmuje tylko elementy <img> znajdujące się między elementami .card i .card__content w drzewie nadrzędnym. Ten typ zakresu, który obejmuje górną i dolną granicę, jest często nazywany zakresem pierścieniowym.

Selektor na stronie :scope

Domyślnie wszystkie reguły stylów zakresu są określone względem podstawowego zakresu. Możesz też kierować reklamy na sam główny element zakresu. Aby to zrobić, użyj selektora :scope.

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

Selektory w regułach stylu zakresu są domyślnie dołączane na początku kolumny :scope. Jeśli chcesz, możesz wyraźnie o tym powiedzieć, dodając na początku :scope. Możesz też dołączyć na początku selektor & w sekcji Zagnieżdżanie 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 korzystać z pseudoklasy :scope, aby wymagać określonej relacji z pierwiastkiem 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 pierwiastka zakresu za pomocą funkcji :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 same reguły stylu zakresu nie mogą wyjść poza drzewo podrzędne. Wybór taki jak :scope + p jest nieprawidłowy, ponieważ powoduje wybranie elementów spoza zakresu.

@scope i specyficzność

Selektory, których użyjesz we wstępie dla funkcji @scope, nie mają wpływu na ich specyfikę. W poniższym przykładzie specyficzność selektora img nadal wynosi (0,0,1).

@scope (#sidebar) {
    img { /* Specificity = (0,0,1) */
        …
    }
}

Szczegółowość :scope jest charakterystyczna dla zwykłej pseudoklasy (0,1,0).

@scope (#sidebar) {
    :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
        …
    }
}

W poniższym przykładzie parametr & jest wewnętrznie przepisany do selektora, który jest używany jako pierwiastka zakresu, umieszczonego wewnątrz selektora :is(). Ostatecznie przeglądarka użyje :is(#sidebar, .card) img jako selektora do dopasowywania. Ten proces jest nazywany deustrakcją.

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        …
    }
}

Ponieważ parametr & jest deuforowany za pomocą funkcji :is(), specyficzność funkcji & jest obliczana według reguł precyzji :is(): specyficzność funkcji & odpowiada najbardziej szczegółowemu argumentowi.

W tym przykładzie specyficzność funkcji :is(#sidebar, .card) odpowiada najbardziej szczegółowemu argumentowi, czyli #sidebar, i dlatego staje się (1,0,0). Gdy połączysz to ze specyfiką funkcji img, czyli (0,0,1), otrzymujemy (1,0,1) jako specyficzność całego złożonego selektora.

@scope (#sidebar, .card) {
    & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
        …
    }
}

Różnica między :scope a & w @scope

Oprócz różnic w sposobie obliczania specyficzności inna różnica między :scope a & polega na tym, że :scope reprezentuje pierwiastek dopasowanego zakresu, a & to selektor służący do dopasowywania pierwiastka zakresu.

Dlatego & można użyć wielokrotnie. Inaczej niż w przypadku parametru :scope, którego możesz użyć tylko raz, ponieważ nie da się dopasować pierwiastka zakresu w obrębie pierwiastka zakresu.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :root :root { /* ❌ Does not work */
    …
  }
}

Zakres bez wprowadzenia

Podczas pisania stylów wbudowanych za pomocą elementu <style> możesz określić zakres reguł stylów do nadrzędnego elementu nadrzędnego elementu <style>, nie określając głównego zakresu. W tym celu należy pominąć wprowadzenie do sekcji @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 powyższym przykładzie reguły zakresu są kierowane tylko na elementy wewnątrz elementu div o nazwie klasy card__header, ponieważ div jest elementem nadrzędnym elementu <style>.

@scope w kaskadzie

W sekcji Kaskada CSS funkcja @scope dodaje też nowe kryterium: zbliżenie zakresu. Krok po szczególności, ale przed kolejnością wystąpienia.

Wizualizacja kaskady CSS.

Zgodnie ze specyfikacją:

Jeśli porównamy deklaracje pojawiające się w regułach stylu z różnymi pierwiastkami zakresu, wygrywa deklaracja z najmniejszą liczbą przeskoków między pierwiastkiem zakresu a obiektem reguły stylu z najmniejszą liczbą przeskoków między pierwiastkiem zakresu a elementem bliźniaczym.

Ten nowy krok jest przydatny podczas zagnieżdżania kilku odmian komponentu. Oto przykład. Element @scope jeszcze nie jest używany:

<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>

Gdy wyświetlisz ten fragment znacznika, trzeci link będzie mieć postać white zamiast black, mimo że jest to element podrzędny elementu div z zastosowaną klasą .light. Wynika to z kolejności kryteriów wyglądu, na podstawie której kaskada wybiera zwycięzcę. Wygląda na to, że domena .dark a została zadeklarowana jako ostatnia, więc wygra w ramach reguły .light a

Kryterium odległości w zakresie pozwala rozwiązać ten problem:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

Oba selektory a mają tę samą specyficzność, więc uruchamiana jest kryterium określania zakresu zbliżonego. Mierzy oba selektory na podstawie ich odległości od pierwiastka zakresu. W przypadku tego trzeciego elementu a jest to tylko 1 przeskok do pierwiastka zakresu .light i 2 do elementu .dark. Dlatego selektor a w tabeli .light wygra.

Uwagi na zakończenie: izolacja selektora, a nie izolacji stylu

Pamiętaj, że @scope ogranicza zasięg selektorów i nie oferuje izolacji stylów. Właściwości dziedziczone w dół do elementów podrzędnych nadal będą dziedziczone (poza dolną granicą @scope). Jedną z takich właściwości jest właściwość color. Gdy zadeklarujesz, że element znajduje się w zakresie pierścieniowym, color nadal będzie dziedziczyć element podrzędny wewnątrz otworu w pierścieniu.

@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ą wartość z .card.

(Zdjęcie na okładkę: rustam burkhanov na stronie Unsplash)