(255,255,255) is the Highest Specificity

Me, at CSS Day 2022, talking about Specificity

# It’s all about that base

CSS Specificity is expressed as a tuple/triad/triple (A,B,C).

  • Count the number of ID selectors in the selector (= A)
  • Count the number of class selectors, attributes selectors, and pseudo-classes in the selector (= B)
  • Count the number of type selectors and pseudo-elements in the selector (= C)
  • Ignore the universal selector

For example, the ID selector #page has a specificity of (1,0,0)

While goofing around in the Chromium Source to see how it’s stored there, I found this interesting piece of code about it:

// We use 256 as the base of the specificity number system.
unsigned Specificity() const;

As the code snippet shows, Blink –Chromium’s underlying rendering engine– stores Specificity as a single number using base 256. This is OK, and is something that used to be mentioned in an older version of the spec:

Concatenating the three numbers a-b-c (in a number system with a large base) gives the specificity.

Now, the choice for 256 seems OK, as it’s quite a large base and also allows for easy value extraction when representing the value in hex. A decimal value of 197121 might not tell you much, but when converted to hex it reads 0x030201 which is more useful. By bit masking and bit shifting, you can easily get the individual values for the A, B, and C components of the Specificity.

unsigned s = 197121;              // 0x030201 in hex
uint8_t a = (s & 0xff0000) >> 16; // 3
uint8_t b = (s & 0x00ff00) >> 8;  // 2
uint8_t c = (s & 0x0000ff);       // 1

So 197121 represents a specificity of (3,2,1)

🤔 Need help calculating Specificity? In my talk The CSS Cascade: A Deep Dive I tell you all about it. Fast forward to the 13 minute mark of the recording.

The Polypane Specificity Calculator is a tool I would also like to recommend. If you want to work with specificity in your own code, go check out @bramus/specificity which I created. Pass in a selector and out comes the specificity.

~

# Exceeding the base

With this base of 256 I wondered: what happens to the Specificity if you exceed the base number? As Kilian Valhof detailed before this can cause problems, because values would overflow into the other component.

For example, a decimal value of 256 represented in hex equals 0x000100. With a base of 256 and using the masking and shifting logic above, that would result in a Specificity of (0,1,0) instead of (0,0,256) … which is not good. It would essentially mean that a selector with a specificity of (0,0,256) would that somehow equal a (0,1,0) selector.

Thankfully this is not the case. When fabricating a selector that has a specificity of (0,0,256) using :not(), we can see it does not beat a (0,1,0) selector

/* (0,1,0) */
.special {
  color: hotpink;
}

/* (0,0,256) */
p:not(a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a) {
  color: blue;
}

See the Pen Specificity (0,0,256) vs (0,1,0) by Bramus (@bramus)on CodePen.

Testing the snippet above the .special will be painted hotpink, so it’s definitely not overflowing. Phew!

~

# Hitting a ceiling

So, what does happen when Blink needs to store a Specificity component of 256? Looking at the source, we can see that Blink clamps the values.

unsigned CSSSelector::Specificity() const {
  // make sure the result doesn't overflow
  static const unsigned kMaxValueMask = 0xffffff;
  static const unsigned kIdMask = 0xff0000;
  static const unsigned kClassMask = 0x00ff00;
  static const unsigned kElementMask = 0x0000ff;

  if (IsForPage()) {
    return SpecificityForPage() & kMaxValueMask;
  }

  unsigned total = 0;
  unsigned temp = 0;

  for (const CSSSelector* selector = this; selector; selector = selector->TagHistory()) {
    temp = total + selector->SpecificityForOneSelector();

    // Clamp each component to its max in the case of overflow.
    if ((temp & kIdMask) < (total & kIdMask)) {
      total |= kIdMask;
    } else if ((temp & kClassMask) < (total & kClassMask)) {
      total |= kClassMask;
    } else if ((temp & kElementMask) < (total & kElementMask)) {
      total |= kElementMask;
    } else {
      total = temp;
    }
  }

  return total;
}

Because of this, each component of the Specificity is limited to a maximum value of 0xFF (255 in decimal). This can be confirmed by creating a page that has selectors with specificity of (0,0,256) and (0,0,255).

/* (0,0,257) ~> (0,0,255) */
p:not(a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a) {
	color: red;
}

/* (0,0,256) ~> (0,0,255) */
p:not(a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a) {
	color: purple;
}

/* (0,0,255) */
p:not(a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a) {
	color: blue;
}

With their specificity clamped to (0,0,255), the cascade will fall back to order of appearance, so the paragraph will be blue.

See the Pen Specificity: (0,0,257) vs (0,0,256) vs (0,0,255) vs (0,0,254) by Bramus (@bramus) on CodePen.

This clamping of values wasn’t always the case. Before this commit authored on 2012-10-04 (Chromium, WebKit) it was possible to have 256 element selectors beat 1 class selector 😱

~

# What about other engines?

Checking the page above in Safari yields the same result. WebKit –with which Blink shares its history– also stores Specificity as a number using base 256. Firefox with its Servo engine on the other hand does not seem to be affected by it. Turning to its source code, we can see it stores Specificity as an unsigned 32 bit integer and uses a 10 bit mask which has a max value of 1024.

const MAX_10BIT: u32 = (1u32 << 10) - 1;

impl From<u32> for Specificity {
    #[inline]
    fn from(value: u32) -> Specificity {
        assert!(value <= MAX_10BIT << 20 | MAX_10BIT << 10 | MAX_10BIT);
        Specificity {
            id_selectors: value >> 20,
            class_like_selectors: (value >> 10) & MAX_10BIT,
            element_selectors: value & MAX_10BIT,
        }
    }
}

Checking this demo in Firefox, you’ll see a blue paragraph, confirming the max specificity there is (1023,1023,1023)

~

# So, what does this all mean?

Nothing much, really. By design CSS Specificity is unlimited. But every system has its limits, and for CSS Specificity in Blink and WebKit that limit is 255. For Servo it is 1023.

In practice you’ll never hit that limit and should you do, I urge you to stop writing bad CSS selectors like that.

~

# Spread the word

To help spread the contents of this post, feel free to retweet its announcement tweet:

~

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Join the Conversation

1 Comment

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.