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

Dowiedz się, jak używać zakresu @, aby wybierać elementy tylko w ograniczonym poddrzewie DOM.

Obsługa przeglądarek

  • Chrome: 118.
  • Edge: 118.
  • Firefox: za pomocą flagi.
  • Safari: 17.4.

Źródło

Delikatna sztuka pisania selektorów CSS

Pisząc selektory, możemy się rozdzielić 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 główny w obszarze treści komponentu karty”, co jest dość specyficznym wyborem elementu, prawdopodobnie nie chcesz pisać selektora takiego jak .card > .content > img.hero.

Nie chcesz jednak wpisywać jako selektora tego elementu tylko img, ponieważ spowodowałoby to wybranie wszystkich elementów obrazu na stronie.

Znalezienie w tym kontekście odpowiedniej równowagi jest często sporym wyzwaniem. Na przestrzeni lat niektórzy programiści opracowali rozwiązania i sposoby obejścia tego problemu, które miały 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 umożliwia 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 generowane 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 stylów bezpośrednio w tagach.

A co, jeśli ich nie potrzebujesz? 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. W tym celu ustaw poziom główny zakresu, który określa górną granicę drzewa podrzędnego, na które chcesz kierować reklamy. Gdy ustawisz ograniczający element skojarzony, zawarte w nim reguły stylów (nazywane ograniczonymi regułami stylów) mogą wybierać tylko z tego ograniczonego poddrzewa DOM.

Aby np. kierować reklamy tylko na elementy <img> w komponencie .card, ustaw .card jako pierwiastek zakresu zakresu reguły @scope.

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

Reguła stylu o zakresie img { … } może w praktyce wybrać tylko te 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.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 o zakresie są określone względem poziomu głównego zakresu. Możesz też kierować reklamy na sam element skojarzony z korzeniami. 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 w sposób wyraźny dodać :scope samodzielnie. Możesz też dołączyć selektor & na początku listy 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 swego 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 poniższym przykładzie wewnętrznie pole & jest przepisywane do selektora służącego do określania poziomu głównego, opakowanego wewnątrz selektora :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: @scope

Oprócz różnic w sposobie obliczania specyficzności :scope& 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: zakres zbliżenia. Ten krok następuje po doprecyzowaniu, ale przed kolejnością występowania.

Wizualizacja kaskady CSS

Zgodnie ze specyfikacją:

Podczas porównywania deklaracji, 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 przydaje się 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 znaczników trzeci link będzie miał wartość white zamiast black, mimo że jest elementem podrzędnym elementu div z zastosowanej klasą .light. Wynika to z kolejności kryterium wyglądu, z której korzysta kaskada w celu wyłonienia 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 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 korzenia zakresu .light jest tylko 1 przeskok, a do elementu .dark – 2 przeskoki. W związku z tym selektor a w tabeli .light wygra.

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