Specificity

Property priority

Plumeria assigns specificity from the shorthand-to-longhand property graph. Each step toward a more specific property adds one :not(#\#) to its atomic class. To simplify the examples, hash(xxxxxxxx) is written as xatom.

Override only that value

This ensures that longhand properties add to shorthand properties, regardless of the merge order:

const styles = css.create({
  base: { margin: 10 },
  custom: { marginTop: 20 },
});

// Both produce the same result
classStyle={styles.base, styles.custom}
classStyle={styles.custom, styles.base}

// Result: margin 10px on all sides, except top is 20px

Normally, a later shorthand can reset an earlier longhand. The specificity hierarchy prevents that reset, so both of the following produce the same result:

xatom1 {
  padding: 20px;
}

xatom2:not(#\#) {
  padding-top: 10px;
}

And this too

xatom2:not(#\#) {
  padding-top: 10px;
}

xatom1 {
  padding: 20px;
}

Logical intermediate properties use the intermediate depth. For example:

padding:             0 - :not(#\#) x 0
padding-block:       1 - :not(#\#) x 1
padding-block-start: 2 - :not(#\#) x 2

Only the property's graph depth is added. A shorthand does not receive extra specificity to simulate the CSS cascade.

Declarations in the same style

Shorthand and longhand declarations remain independent atoms even when they are written in the same style. A later shorthand does not filter out an earlier longhand:

const styles = css.create({
  base: {
    paddingTop: '20px',
    padding: '10px',
  },
});

The longhand's higher specificity keeps the top value while the shorthand sets the remaining sides:

xatom1:not(#\#) {
  padding-top: 20px;
}

xatom2 {
  padding: 10px;
}

Enforcing with ESLint

These relationships are complex even in regular CSS. The ESLint rule enforces a consistent authoring order by placing longhand properties after their shorthands.

classStyle prop merge styles

Also, when classStyle prop has duplicate properties on its array, the style properties on the right always take precedence and duplicate properties on the left are filtered. In that case no filtered hash is generated.

const styles = css.create({
  base: { 
    fontSize: 10,
  },
  custom: { 
    fontSize: 16,
  },
});

// The right always wins
classStyle={styles.base, styles.custom} // 16px
classStyle={styles.custom, styles.base} // 10px

Generated as follows:

xatom {
  font-size: 16px;
}

Or:

xatom {
  font-size: 10px;
}

Conditional and nested rules

Conditional rules such as @media, @container, and @supports, as well as nested selectors, receive one additional :not(#\#). This lets a conditional or nested declaration override its base declaration without relying on where the generated rule appears in the stylesheet. A nested selector inside a condition receives both.

base shorthand:                property depth 0
base longhand:                 property depth
shorthand in condition:        property depth 0 + condition depth 1
longhand in condition:         property depth + condition depth 1
nested selector:               property depth + nested depth 1
nested selector in condition:  property depth + nested depth 1 + condition depth 1

Custom properties are exempt. They never receive a nested or condition depth.

The optimizer merges identical selectors and identical at-rules recursively to reduce bundle size. Supported conditional at-rules are moved after base rules so a conditional shorthand can override a base longhand when their specificity is equal. A condition that matches a subset of another is then placed after it, so the narrower one wins everywhere both of them reach. Whether one condition implies another is not decidable in general, so only the comparable shapes are read — a min-, max- or range comparison on width, height, inline-size or block-size, joined with and, against the same media type or the same container. Anything else keeps the order it was written in, unless a comparable pair leaves no arrangement that also holds it still.

What source order still decides

Every pair where one declaration contains the other is settled by specificity, so composing styles from different modules gives the same result no matter which module the bundler reached first. Three pairs are left to source order, and all three are pairs where no containment exists to rank.

The same property under two names

paddingTop and paddingBlockStart are the same computed property in a horizontal writing mode, so they receive the same depth and neither can outrank the other. Plain CSS resolves this the same way.

In a vertical writing mode they are two different edges and no longer compete. Which one an element renders under is decided at runtime and inherited, so no build-time setting can answer it; no-order-dependent-overlap reads the pair as one property, which is right for a horizontal or a mixed document and wrong only where an element is known to be vertical. Writing one spelling and not the other removes the question, and no-physical-properties enforces that.

const styles = css.create({
  physical: { paddingTop: 4 },
  logical: { paddingBlockStart: 10 },
});

Two shorthands that cross

borderTop sets three values on one edge. borderBlockWidth sets one value on two edges. They overlap on border-top-width, but neither contains the other, so there is nothing to rank them by. Writing the three declarations borderTop stands for turns the pair into a shorthand and a longhand, which specificity does rank; expand-border-shorthands does that rewrite.

const styles = css.create({
  edge: { borderTop: '1px solid red' },
  axis: { borderBlockWidth: 5 },
});

Two conditions that overlap

Two conditions that can match at the same time produce the same specificity. Where one of them matches a subset of the other, the optimizer settles the pair by placing the narrower one last, so the result no longer depends on where the two declarations were written or which module the compiler reached first.

const styles = css.create({
  medium: { '@media (min-width: 600px)': { color: 'red' } },
  wide: { '@media (min-width: 900px)': { color: 'blue' } },
});

At a width of 1000px both queries apply and blue wins, written in either order. no-order-dependent-overlap still reports the reversed spelling, because the source then reads as though red would win.

Two conditions that overlap without either containing the other — 600px to 900px against 700px to 1000px — are left to source order, and so is any pair the optimizer cannot compare.

Keep these in one style

Source order is stable inside a single style object, and the ESLint rule keeps that order consistent. Splitting one of these pairs across two css.create calls is what makes the result depend on the bundler instead. no-order-dependent-overlap reports all three when they meet in one style, the conditions only where one provably matches a subset of the other.

Removing the first two entirely

Two optional rules take the room away instead of reporting it. Expanding a border shorthand into the three declarations it stands for turns a crossing pair into a shorthand and a longhand, and writing one spelling of a property and not the other means the same property can never appear twice:

import plumeria from '@plumeria/eslint-plugin';

export default [
  plumeria.configs.recommended,
  {
    rules: {
      '@plumeria/expand-border-shorthands': 'warn',
      '@plumeria/no-physical-properties': 'warn',
    },
  },
];

With both on, every pair on this page except the overlapping conditions becomes impossible to write. See Optional rules.

On this page