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

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

Obsługa przeglądarek

  • Chrome: 118.
  • Edge: 118.
  • Firefox: za flagą.
  • Safari: 17.4

Źródło

Delikatna sztuka pisania selektorów CSS

Pisząc selektory, możemy się rozdzielić między 2 światami. Z drugiej strony musisz bardzo precyzyjnie wybierać elementy, które wybierasz. Z drugiej strony selektory powinny być łatwe do zastąpienia i nie będą ściśle powiązane ze strukturą DOM.

Jeśli na przykład chcesz wybrać „główny obraz w obszarze treści komponentu karty” – czyli jest to raczej specyficzny wybór elementów – raczej nie warto zapisywać selektora takiego jak .card > .content > img.hero.

  • Ten selektor ma dość wysoki szczegółowość, czyli (0,3,1), co utrudnia jego 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 ulegną zmianie, musisz również zmienić CSS.

Nie warto jednak zapisywać parametru img jako selektora tego elementu, ponieważ spowoduje to wybranie wszystkich elementów graficznych 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ą nadania temu elementowi klasy card__img card__img--hero, aby jego poziom szczegółowości był niski, a jednocześnie zawierał precyzyjne informacje.
  • Rozwiązania oparte na języku JavaScript, takie jak Zakresowy kod CSS lub Komponenty ze stylem, zmieniają wszystkie selektory, dodając do selektorów losowo generowane ciągi znaków (np. sc-596d7e0e-4), aby uniemożliwić im kierowanie reklam na elementy po drugiej stronie strony.
  • Niektóre biblioteki całkowicie rezygnują z selektorów i wymagają umieszczenia reguł określania stylu bezpośrednio w samych znacznikach.

A co, jeśli ich nie potrzebujesz? Co by było, gdyby CSS pozwalał dokładnie określać, które elementy wybierasz, bez konieczności pisania selektorów o dużej szczegółowości lub ściśle powiązanych z DOM? Właśnie tu do akcji wkracza usługa @scope, która umożliwia wybieranie elementów tylko w obrębie poddrzewa DOM.

Przedstawiamy @scope

@scope pozwala 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 zakres główny zakresu, zawarte w nim reguły stylu – nazywane regułami stylu zakresu – 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 z zakresem img { … } może w praktyce wybrać tylko te elementy <img>, które są w zakresie dopasowanego elementu .card.

Aby uniemożliwić wybieranie elementów <img> w obszarze treści karty (.card__content), możesz bardziej sprecyzować 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 z ograniczonym zakresem jest kierowana tylko na elementy <img>, które znajdują się między elementami .card i .card__content w drzewie nadrzędnym. Ten rodzaj określania zakresu – z górną i dolną granicą – jest często nazywany zakresem pierścieniowym.

Selektor :scope

Domyślnie wszystkie reguły stylu o zakresie są określane względem poziomu głównego zakresu. Możesz też kierować reklamy na sam element główny określający zakres. 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 o ograniczonym zakresie są domyślnie dołączane na początku elementu :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 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 używać pseudoklasy :scope, aby wymagać określonej relacji z główną wartością określającą zakres:

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

Limit zakresu może też odwoływać się do elementów spoza poziomu głównego 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 o zakresie nie mogą wyjść z drzewa podrzędnego. Elementy takie jak :scope + p są nieprawidłowe, ponieważ próbują wybrać elementy, które nie znajdują się w zakresie.

@scope i szczegółowość

Selektory użyte we wstępie do elementu @scope nie wpływają na szczegółowość zawartych w nim selektorów. W poniższym przykładzie dokładność selektora img nadal jest (0,0,1).

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

Specyfika klasy :scope jest pseudoklasą zwykłej, 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(). Na koniec przeglądarka użyje :is(#sidebar, .card) img jako selektora do dopasowywania. Ten proces jest nazywany usunięciem ubrań.

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

Ponieważ konstrukcja & jest odsłuchiwana za pomocą metody :is(), dokładność & jest obliczana zgodnie z regułami szczegółowości :is(). Szczegółowość & jest tym, że najbardziej precyzyjna jest jego najbardziej precyzyjna wartość.

W tym przykładzie specyficzność funkcji :is(#sidebar, .card) wynika z najbardziej szczegółowego argumentu, czyli #sidebar, i dlatego wartość wynosi (1,0,0). Jeśli połączysz to ze specyfiką parametru img, czyli (0,0,1), otrzymasz (1,0,1) jako dokładność całego selektora złożonego.

@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 szczegółowości inna różnica między wartościami :scope a & polega na tym, że :scope reprezentuje dopasowany pierwiastek zakresu, a & reprezentuje selektor służący do dopasowania pierwiastka zakresu.

Z tego względu możliwe jest kilkukrotne użycie tagu &. W przeciwieństwie do funkcji :scope, której można użyć tylko raz, ponieważ pierwiastek określający zakres nie można dopasować do pierwiastka zakresu.

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

Zakres bez wstępu

Jeśli piszesz style wbudowane z elementem <style>, reguły stylu możesz ograniczyć do nadrzędnego elementu <style>, nie określając poziomu głównego zakresu. W tym celu pomijasz wstęp do dokumentu @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 zakresie 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 kaskadzie usług porównywania cen @scope dodaje też nowe kryterium: odległość zakresu do zakresu. Ten krok następuje po doprecyzowaniu, ale przed kolejnością występowania.

Wizualizacja kaskady CSS.

Zgodnie ze specyfikacją:

Gdy porównujesz deklaracje, które występują w regułach stylu z różnymi poziomami zakresu, wygrywa deklaracja z najmniejszą liczbą przeskoków pokolenia lub elementów równorzędnych między elementem nadrzędnym a zasadą stylu zakresu.

Ten nowy krok przydaje się, gdy zagnieżdżasz kilka odmian komponentu. Oto przykład, który jeszcze nie używa języka @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>

Gdy wyświetlisz ten mały znacznik, trzeci link będzie miał postać white zamiast black, mimo że jest to element podrzędny elementu div z zastosowaną do niego klasą .light. Wynika to z kolejności kryterium wyglądu, z której korzysta kaskada w celu wyłonienia zwycięzcy. widzi, że pole .dark a zostało zadeklarowane jako ostatnie, więc wygra zgodnie z regułą .light a

W przypadku kryterium kierowania na zbliżony obszar ten problem został już rozwiązany:

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

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

Oba selektory a o zakresie mają taką samą dokładność, dlatego włącza się kryterium zbliżonego obszaru specjalizacji. Waży oba selektory pod względem odległości od ich pierwiastka zakresu. W przypadku trzeciego elementu a oznacza to tylko 1 przeskok do poziomu głównego zakresu .light, ale 2 przeskok do poziomu .dark. W związku z tym selektor a w tabeli .light wygra.

Uwagi końcowe: izolacja selektora, a nie izolacja stylu

Warto pamiętać, że @scope ogranicza zasięg selektorów, a nie izolacja stylów. Właściwości dziedziczące po elementach podrzędnych będą nadal dziedziczone, wykraczając poza dolną granicę właściwości @scope. Jedna z takich właściwości to color. Po zadeklarowaniu, że element znajduje się w zakresie pierścienia, właściwość color odziedziczy po elementach podrzędnych.

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

(Zdjęcie na okładkę: rustam burchhanov w Unsplash)