Plumeria

GUIDES

Selector rules

How pseudo-classes, pseudo-elements and queries nest inside each other, and why.

Nesting

A style nests one level of selectors, and only in one direction:

  • A query (@media or @container) can hold a pseudo-selector.
  • A pseudo-selector cannot hold a query, and a query cannot hold another query.
  • A pseudo-selector cannot hold another pseudo-selector. Write the pair as one key, such as ':hover::before'.
TypeScript
import * as css from '@plumeria/core';

const styles = css.create({
  list: {
    '@media (max-width: 768px)': {
      ':hover': {
        listStyleType: 'none',
      },
    },
  },
});

Not allowed:

TypeScript
import * as css from '@plumeria/core';

const styles = css.create({
  list: {
    ':hover': {
      '@media (max-width: 768px)': {
        background: 'skyblue',
      },
    },
  },
});

Combinators

A key cannot contain a combinator (>, +, ~ or a space) except inside a functional pseudo-class such as :has(). To style a child from its parent's state, use marker and extended.

no-invalid-selector and no-combinator in @plumeria/eslint-plugin report each of these.

Functional pseudo-classes

A functional pseudo-class cannot appear inside another one of the same name, at any depth. Different names can nest, such as ':is(:where(:not(:has(.a))))'. :not() is the exception: it may nest inside :not(), because ':not(:not(.a), .b)' has no flat form.

':where(:where(.a, .b), .c)'
':where(.a, .b, .c)'
':is(:where(:is(.a)))'

The compiler stops the build on such a key. no-invalid-selector flattens it with a fix when the nested call is a whole argument, as in the first line; any other shape has to be rewritten by hand, because flattening would change what the selector matches.

A key can nest at most 16 levels of parentheses, and a quote must open and close a string inside brackets or parentheses. The compiler stops the build on a deeper key or a stray quote such as ':hover":is(.x)', and no-invalid-selector reports both.

Reason

Every rule a style can produce has exactly one way to be written. A stylesheet places a selector inside a query — @media (…) { .a:hover { … } } — never the reverse, so the key order follows the CSS it compiles to.

Allowing the reverse would give the same rule two spellings, and the style's type would have to recurse through every pseudo-selector and query to accept both. Keeping one level in one direction keeps the type flat, its autocomplete precise, and every style in a codebase nested the same way.

On this page