htmlcsssasscss-selectors

How to select a descendent of div.box-type-1, but only when div.box-type-1 is the closest ancestor with a .box-type-* class?


I’ve got containers with colored backgrounds that fall into one of three different categories, represented by the sass placeholders %dark-box, %light-box, and %gradient-box. I need to adjust the styling of elements that are descendants of these boxes. These elements can be nested at any arbitrary depth within the box.

The problem is that the boxes can be nested inside each other, also at any arbitrary depth, and there’s no limit to how many levels deep the nesting can go. Boxes, inside boxes, inside boxes, etc. So the selectors targeting elements inside the boxes should only target elements when that box is the nearest ?-box ancestor.

Consider this HTML:

<div class="dark-box"> <!-- backgrund-color:$dark-box-bg-color -->
  <div>
    <div>
      <div>
        <p>This should have color:$dark-box-text-color</p>
      </div>
    </div>
  </div>
  <div>
    <div class="light-box"> <!-- backgrund-color:$light-box-bg-color -->
      <div>
        <div>
          <div>
            <p>This should have color:$light-box-text-color</p>
          </div>
          <div>
            <p>This should have color:$light-box-text-color</p>
          </div>
          <div>
            <div class="dark-box"> <!-- backgrund-color:$dark-box-bg-color -->
              <div>
                <div class="light-box"> <!-- backgrund-color:$light-box-bg-color -->
                  <div>
                    <div class="dark-box"> <!-- backgrund-color:$dark-box-bg-color -->
                      <div>
                        <div>
                          <p>This should have color:$dark-box-text-color</p>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

With this scss:

%dark-box {
  background-color: $dark-box-bg-color;
  // other stuff ...
}

%light-box {
  background-color: $light-box-bg-color;
  // other stuff ...
}

%gradient-box {
  background-color: $gradient-box-bg-color;
  // other stuff ...
}

.dark-box {@extend %dark-box;}
.light-box {@extend %light-box;}
.gradient-box {@extend %gradient-box;}

%dark-box-text {
  color: $dark-box-text-color;
  // other stuff ...
}

%light-box-text {
  color: $light-box-text-color;
  // other stuff ...
}

%gradient-box-text {
  color: $gradient-box-text-color;
  // other stuff ...
}

:is(%dark-box) > p,
:is(%dark-box) > /*! n descendants, none of which have a %light-box or %gradient-box class */ > p {
  @extend %dark-box-text;
}

:is(%light-box) > p,
:is(%light-box) > /*! n descendants, none of which have a %dark-box or %gradient-box class */ > p {
  @extend %light-box-text;
}

:is(%gradient-box) > p,
:is(%gradient-box) > /*! n descendants, none of which have a %dark-box or %light-box class */ > p {
  @extend %gradient-box-text;
}

How can I craft a selector that targets the first and last paragraph, but neither of the paragraphs in the middle? And vice versa?

I know I could do something that explicitly targets every possible depth that a paragraph could be at (like shown below), but then I would have to enforce a maximum depth limit. It would also mean an unacceptable increase in sass preprocessing time, when you consider that the box placeholders could be extended by a number of different selectors, and paragraphs are just one of many elements that I’ll need to target in this way.

:is(%dark-box) > p,
:is(%dark-box) > :not(:where(%light-box), :where(%gradient-box)) > p,
:is(%dark-box) > :not(:where(%light-box), :where(%gradient-box)) > :not(:where(%light-box), :where(%gradient-box)) > p,
:is(%dark-box) > :not(:where(%light-box), :where(%gradient-box)) > :not(:where(%light-box), :where(%gradient-box)) > :not(:where(%light-box), :where(%gradient-box)) > p,
// etc...
{
  @extend %dark-box-text;
}

I've tried some variations of the following, but haven't found anything that works yet.

:is(%dark-box) > p,
:is(%dark-box) :not(:where(:where(%light-box), :where(%gradient-box))) p {
  @extend %dark-box-text;
}

Solution

  • Unfortunately it sounds like you’re running into one of the problems that will, in the future, be solved by the @scope at-rule.

    That said, while selectors don’t account for nearest-ancestor proximity, inheritance does. So you can use CSS custom properties to solve this.

    div {
      padding: 4px;
    }
    
    .dark-box, .light-box {
      color: var(--text-color);
      background-color: var(--background-color);
    }
    
    .dark-box {
      --text-color: white;
      --background-color: midnightblue;
    }
    
    .light-box {
      --text-color: black;
      --background-color: lemonchiffon;
    }
    <div class="dark-box"> <!-- backgrund-color:$dark-box-bg-color -->
      <div>
        <div>
          <div>
            <p>This should have color:$dark-box-text-color</p>
          </div>
        </div>
      </div>
      <div>
        <div class="light-box"> <!-- backgrund-color:$light-box-bg-color -->
          <div>
            <div>
              <div>
                <p>This should have color:$light-box-text-color</p>
              </div>
              <div>
                <p>This should have color:$light-box-text-color</p>
              </div>
              <div>
                <div class="dark-box"> <!-- backgrund-color:$dark-box-bg-color -->
                  <div>
                    <div class="light-box"> <!-- backgrund-color:$light-box-bg-color -->
                      <div>
                        <div class="dark-box"> <!-- backgrund-color:$dark-box-bg-color -->
                          <div>
                            <div>
                              <p>This should have color:$dark-box-text-color</p>
                            </div>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    Also, I should point out that class names don’t get the . dot in the html markup; it’s only for constructing the CSS selector.