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

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

Opublikowano: 4 października 2023 r.

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: 146.
  • Safari: 17.4.

Source

Podczas pisania selektorów możesz mieć problem z wyborem między dwoma światami. Z jednej strony chcesz dokładnie określić, które elementy wybierasz. Z drugiej strony chcesz, aby selektory można było łatwo zastępować i aby nie były ściśle powiązane ze strukturą DOM.

Jeśli np. chcesz wybrać „obraz główny w obszarze treści komponentu karty” (co jest dość szczegółowym wyborem elementu), prawdopodobnie nie chcesz pisać selektora takiego jak .card > .content > img.hero.

  • Ten selektor ma dość wysoką specyficzność wynoszącą (0,3,1), co utrudnia jego zastąpienie w miarę rozrastania się kodu.
  • Ze względu na użycie kombinatora bezpośredniego elementu podrzędnego jest on ściśle powiązany ze strukturą DOM. Jeśli znaczniki ulegną zmianie, musisz też zmienić CSS.

Nie chcesz jednak używać selektora img, ponieważ spowoduje to wybranie wszystkich elementów obrazu na stronie.

Znalezienie odpowiedniej równowagi jest często dość trudne. Z biegiem lat niektórzy deweloperzy opracowali rozwiązania i obejścia, które mogą Ci pomóc w takich sytuacjach. Na przykład:

  • Metodologie takie jak BEM wymagają, aby element miał klasę card__img card__img--hero, co pozwala zachować niską specyficzność, a jednocześnie umożliwia precyzyjne określenie, co chcesz wybrać.
  • Rozwiązania oparte na JavaScript, takie jak Scoped CSS czy Styled Components, przepisują wszystkie selektory, dodając do nich losowo wygenerowane ciągi znaków, np. sc-596d7e0e-4, aby uniemożliwić im kierowanie na elementy po drugiej stronie strony.
  • Niektóre biblioteki całkowicie eliminują selektory i wymagają umieszczania wyzwalaczy stylów bezpośrednio w samym znaczniku.

Co jednak, jeśli nie potrzebujesz żadnego z tych elementów? Co by było, gdyby CSS dawał Ci możliwość precyzyjnego określania elementów, które chcesz wybrać, bez konieczności pisania selektorów o wysokiej specyficzności lub ściśle powiązanych z DOM? W takiej sytuacji przydaje się @scope, które 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. Po ustawieniu elementu głównego zakresu zawarte w nim reguły stylu – zwane regułami stylu o ograniczonym zakresie – mogą wybierać tylko z tego ograniczonego poddrzewa DOM.

Jeśli na przykład chcesz kierować reklamy tylko na elementy <img> w komponencie .card, ustaw .card jako korzeń zakresu reguły @ @scope.

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

Reguła stylu z zakresem img { … } może skutecznie wybierać tylko elementy <img>, które są w zakresie pasującego elementu .card.

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

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

Ta reguła stylu o ograniczonym zakresie dotyczy tylko elementów <img>, które znajdują się między elementami .card i .card__content w drzewie elementów nadrzędnych. Ten typ zakresu, z górną i dolną granicą, jest często określany jako zakres w kształcie pączka.

Selektor :scope

Domyślnie wszystkie reguły stylu o określonym zakresie są względne w stosunku do elementu głównego zakresu. Można też kierować reklamy na sam element główny zakresu. W tym celu 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 stylów o ograniczonym zakresie mają niejawnie dodany prefiks :scope. Jeśli chcesz, możesz to wyraźnie zaznaczyć, dodając na początku :scope. Możesz też dodać selektor & 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ślonej relacji z elementem głównym zakresu:

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

Ograniczenie zakresu może też odwoływać się do elementów poza jego głównym elementem, używając :scope. Na przykład:

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

Same reguły stylów z zakresem nie mogą wyjść poza poddrzewo. Wyrażenia takie jak :scope + p są nieprawidłowe, ponieważ próbują wybrać elementy, które nie są w zakresie.

@scope i zgodność ze specyfikacją

Selektory użyte w preludium do @scope nie mają wpływu na specyficzność zawartych w nim selektorów. W naszym przykładzie specyficzność selektora img nadal wynosi (0,0,1).

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

Specyficzność selektora :scope jest taka sama 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 & jest przekształcany w selektor używany w przypadku elementu głównego zakresu, który jest zawarty w selektorze :is(). Przeglądarka użyje ostatecznie selektora :is(#sidebar, .card) img do dopasowania. Ten proces jest nazywany odcukrzaniem.

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

Ponieważ & jest rozwijany za pomocą :is(), jego specyficzność jest obliczana zgodnie z zasadami specyficzności :is(): specyficzność & jest równa specyficzności jego najbardziej szczegółowego argumentu.&

W tym przykładzie specyficzność funkcji :is(#sidebar, .card) jest równa specyficzności jej najbardziej szczegółowego argumentu, czyli #sidebar, a więc wynosi (1,0,0). Połącz to ze specyficznością img, która wynosi (0,0,1), a otrzymasz (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&@scope

Oprócz różnic w sposobie obliczania specyficzności :scope& różnią się tym, że :scope reprezentuje dopasowany element główny zakresu, a & – selektor użyty do dopasowania elementu głównego zakresu.

Dlatego można użyć & kilka razy. W przeciwieństwie do :scope, którego możesz użyć tylko raz, ponieważ nie możesz dopasować korzenia zakresu w korzeniu 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ć reguły stylu do elementu nadrzędnego zawierającego element <style>, nie określając żadnego katalogu głównego 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 powyższym przykładzie reguły o ograniczonym zakresie dotyczą tylko elementów w elemencie div o nazwie klasy card__header, ponieważ ten element div jest elementem nadrzędnym elementu <style>.

@scope w kaskadzie

kaskadowym arkuszu stylów CSS zasada @scope dodaje też nowe kryterium: bliskość zakresu. Ten krok następuje po szczegółowości, ale przed kolejnością wyświetlania.

Wizualizacja kaskady CSS.

Zgodnie ze specyfikacją:

Podczas porównywania deklaracji występujących w regułach stylu z różnymi korzeniami zakresu wygrywa deklaracja z najmniejszą liczbą przeskoków między korzeniem zakresu a elementem podlegającym regule stylu w przypadku elementów pokoleniowych lub sąsiednich.

Ten nowy krok przydaje się, gdy zagnieżdżasz kilka wariantów komponentu. Oto przykład, w którym nie użyto jeszcze funkcji @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 kodu trzeci link będzie miał wartość white zamiast black, mimo że jest elementem podrzędnym elementu div z zastosowaną klasą .light. Wynika to z kryterium kolejności wyświetlania, które jest używane w tym przypadku do wyłonienia zwycięzcy. Widzi, że .dark a został ogłoszony jako ostatni, więc wygrywa zgodnie z zasadą .light a.

Dzięki kryterium zasięgu bliskości problem ten został rozwiązany:

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

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

Ponieważ oba selektory z zakresem a mają taką samą specyficzność, zaczyna działać kryterium bliskości zakresu. Waga selektorów zależy od ich odległości od elementu głównego zakresu. W przypadku trzeciego elementu a do elementu głównego zakresu .light jest tylko jeden przeskok, a do elementu .dark – dwa. Dlatego selektor a.light wygra.

Odizolowanie selektora, a nie stylu

Pamiętaj, że @scope ogranicza zasięg selektorów. Nie oferuje izolacji stylu. Właściwości, które są dziedziczone przez usługi podrzędne, nadal są dziedziczone, nawet jeśli przekraczają dolną granicę @scope. Jedną z takich właściwości jest color. Gdy zadeklarujesz, że jeden z nich znajduje się w zakresie pączka, color nadal dziedziczy w dół do elementów podrzędnych wewnątrz dziurki pączka.

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

W tym przykładzie element .card__content i jego elementy podrzędne mają kolor hotpink, ponieważ dziedziczą wartość z elementu .card.