Search code examples
csscss-selectorspseudo-class

In CSS, how may I select elements which are *not* the descendants of a container element with a class conforming to a recognisable pattern?


Imagine that the HTML and CSS below is already set.

What CSS rules can I add beneath the already-written CSS to make the red paragraphs display as red?

body,
div {
  display: flex;
  flex-wrap: wrap;
}

p {
  margin: 6px;
}

.one-filter-one p,
p[class^="one-filter-one"] {
  color: blue;
}

.two-filter-two p,
p[class^="two-filter-two"] {
  color: green;
}

.four-filter-four p,
p[class^="four-filter-four"] {
  color: orange;
}
<p class="another-class">This is red.</p>
<p>This is red.</p>

<div class="one-filter-one">
  <p>This is blue.</p>
  <p class="one-filter-one--paragraph">This is blue.</p>
</div>

<p class="two-filter-two">This is green.</p> 

<p>This is red.</p> 
<p class="another-class-two">This is red.</p>

<div class="three-filter-three">
  <p>This is unstyled (black).</p>
  <div><p>This is unstyled (black) too.</p></div> 
</div>

<div class="four-filter-four">
  <p class="four-filter-four--sentence">This is orange.</p>
</div>
  
<p class="five-filter-five">This is also unstyled (black).</p>

<div class="another-class-three">
  <p>This is red.</p>
  <p class="another-class-four">This is red.</p>
</div>


My best guess is to use the :not() pseudo-class.

But I'm not entirely convinced this is the right approach, principally because I'm not sure that :not() can handle this case.

My attempt at a solution, using :not():

body,
div {
  display: flex;
  flex-wrap: wrap;
}

p {
  margin: 6px;
}

.one-filter-one p,
p[class^="one-filter-one"] {
  color: blue;
}

.two-filter-two p,
p[class^="two-filter-two"] {
  color: green;
}

.four-filter-four p,
p[class^="four-filter-four"] {
  color: orange;
}

p:not([class*="-filter-"]) {
  color: red;
}
<p class="another-class">This is red.</p>
<p>This is red.</p>

<div class="one-filter-one">
  <p>This is blue.</p>
  <p class="one-filter-one--paragraph">This is blue.</p>
</div>

<p class="two-filter-two">This is green.</p> 

<p>This is red.</p> 
<p class="another-class-two">This is red.</p>

<div class="three-filter-three">
  <p>This is unstyled (black).</p>
  <div><p>This is unstyled (black) too.</p></div> 
</div>

<div class="four-filter-four">
  <p class="four-filter-four--sentence">This is orange.</p> </div>
  
<p class="five-filter-five">This is also unstyled (black).</p>

<div class="another-class-three">
  <p>This is red.</p>
  <p class="another-class-four">This is red.</p>
</div>

Clearly this is not it, because I am not correctly selecting:

NOT descendant elements of [class*="-filter-"].

But I'm not clear how to do this at all.

Is there any way to do this, or am I looking to achieve the impossible in 2020, given CSS's contemporary capabilities?


Notes:

Although, in 2020, the pseudo-class :not() has been around for the best part of a decade I've always tended to avoid using it. The only thing I do know is that the :not() pseudo-class function can only take simple (ie. not compound) selectors.


Added:

Based on @G-Cyrillus' brilliant suggestion (in the comments, immediately below), I have come up with the following:

body > p:not([id*="-filter-"]):not([class*="-filter-"]),
body > :not([class*="-filter-"]) > p:not([id*="-filter-"]):not([class*="-filter-"]),
body > :not([class*="-filter-"]) > :not([class*="-filter-"]) > p:not([id*="-filter-"]):not([class*="-filter-"]),
body > :not([class*="-filter-"]) > :not([class*="-filter-"]) > :not([class*="-filter-"]) > p:not([id*="-filter-"]):not([class*="-filter-"]),
body > :not([class*="-filter-"]) > :not([class*="-filter-"]) > :not([class*="-filter-"]) > :not([class*="-filter-"]) > p:not([id*="-filter-"]):not([class*="-filter-"]) {
  color: red;
}

On the plus side this does work. (So, infinitely better than anything I had before).

On the minus side:

  • it's verbose
  • it's inelegant
  • it only works to the fourth level of element-nesting
  • I can of course carry on adding levels, but that only makes it verboser and ineleganter

Solution

  • This has been a educational exercise.

    The most significant thing it's taught me is that, given that :not() cannot accept compound selectors, it's very far from straightforward to handle subsequent nested levels of markup after applying :not().

    Given the following:

    .filter-1 {
      color: red;
    }
    
    :not([class^="filter-"]) p {
      color: blue;
    }
    <div>
      <div>
        <p>Test.</p>
      </div>
    </div>
    
    <div class="filter-1">
      <div>
        <p>Test.</p>
      </div>
    </div>

    the second <p> still shows up blue.

    Why? Because even though its grandparent has the class .filter-1, its immediate parent does not... and that's enough to satisfy the any descendant selector (ie. the [SPACE]) preceding the p in the CSS Rule:

    :not([class^="filter-"]) p
    

    The only way to get around this is to replace the rule with:

    :not([class^="filter-"]) > * > p
    

    and this now works:

    .filter-1 {
      color: red;
    }
    
    :not([class^="filter-"]) > * > p {
      color: blue;
    }
    <div>
      <div>
        <p>Test.</p>
      </div>
    </div>
    
    <div class="filter-1">
      <div>
        <p>Test.</p>
      </div>
    </div>

    But...

    the CSS Rule is now tightly bound to the HTML structure and the amended CSS rule above won't now apply to:

    <div class="filter-2">
      <p>Test.</p>
    </div>
    

    See:

    .filter-1 {
      color: red;
    }
    
    :not([class^="filter-"]) > * > p {
      color: blue;
    }
    <div>
      <div>
        <p>Test.</p>
      </div>
    </div>
    
    <div class="filter-1">
      <div>
        <p>Test.</p>
      </div>
    </div>
    
    <div class="filter-2">
      <p>Test.</p>
    </div>

    Instead, we now need to use two rules:

    :not([class^="filter-"]) > p,
    :not([class^="filter-"]) > * > p
    

    The following conclusion emerges:

    We can only use :not() to exclude descendants when we also explicitly describe the HTML structure in the CSS.

    I now understand much more clearly what @G-Cyrillus meant by:

    You need to mind the structure too


    Next Steps:

    Describing an infinite number of potential descendant structures in my CSS is clearly impractical, so I've:

    1) reconfigured my architecture to allow more complex descendant relationships to be described elsewhere

    and

    2) optimised my exclusion query to:

    body > :not([id^="filter-"]):not([class^="filter-"])
    

    Thanks very much again, @G-Cyrillus - I've only made it as far as this due to your substantial assistance in the comment section.