Search code examples
csscss-selectors

Can you optionally apply a class to a complex selector without repeating the complex selector or declarations?


Context

The following CSS rules are used in a theme that multiple sites share. Some sites have menu items while others don't. These are styles applied to an element with the header class before it is eventually mounted by a JS web component, after which it will have multiple child elements and thus will be unaffected by the styles. Sites that do not have a menu attach the no-menu class as well.

Just vanilla CSS without preprocessors.

Question

I have the following CSS rule:

:is(.header:empty,.header:has(div:only-child)) {
  --header-min-height: 114px;
  --header-title-height: 63px;
  &.no-menu {
    --header-min-height: 101px;
    --header-title-height: 50px;
  }
}

/* If on a smaller screen, a hamburger nav appears, so
   apply the "no-menu" styles to both versions. */
@media (max-width: 1100px) {
  :is(.header:empty,.header:has(div:only-child)) {
    --header-min-height: 101px;
    --header-main-gradient: 50px;
  }
}

Due to specificity, the media query rule does not affect the no-menu variant on smaller screens.

Is there a way to adjust the media-query-contained rule to address this issue exclusively by manipulating the selectors and without introducing duplicate information?

The following solutions solve the problem, but introduce duplication that I'd like to avoid.

Bad Solution 1

Duplicating the selector and adding .no-menu to the end of it:

@media (max-width: 1100px) {
  :is(.header:empty,.header:has(div:only-child)),
  :is(.header:empty,.header:has(div:only-child)).no-menu {
    --header-min-height: 101px;
    --header-main-gradient: 50px;
  }
}

Bad Solution 2

Add a nested CSS rule of &.no-menu { ... } (duplicates the declarations):

@media (max-width: 1100px) {
  :is(.header:empty,.header:has(div:only-child)) {
    --header-min-height: 101px;
    --header-main-gradient: 50px;
    &.no-menu {
      --header-min-height: 101px;
      --header-main-gradient: 50px;
    }
  }
}

Solution

  • Happened upon this solution while experimenting and was surprised it works.

    I thought to myself that, in the ideal case, you'd be able to leverage CSS nesting, but have the nested rule apply to both the current scope as well as the context with the class. Turns out, you can use :scope with :is() to achieve this:

    @media (max-width: 1100px) {
      :is(.header:empty,.header:has(div:only-child)) {
        &:is(:scope,.no-menu) {
          --header-min-height: 101px;
          --header-main-gradient: 50px;
        }
      }
    }