4

Comparing CSS Specificity values

 2 years ago
source link: https://kilianvalkhof.com/2022/css-html/comparing-css-specificity-values/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Comparing CSS Specificity values

Kilian Valkhof

Building tools that make developers awesome.

Search term

Comparing CSS Specificity values

CSS & HTML, Javascript, 17 January 2022, 5 minute read

CSS Specificity is usually written out as [a,b,c] for ID’s, classes and elements respectively. Even a single ID is more specific than any number of classes or elements, so it’s displayed as an array and can’t really be fitted into a single number. How do you compare two selectors to decide which has the highest specificity?

CSS Specificity

If you haven’t calculated CSS Specificity before, here’s a quick example:

The selector header h1#sitetitle > .logo has:

  • 1 ID (#sitetitle)
  • 1 class (.logo)
  • 2 elements (header and h1)
  • A child selector (>) which has no specificity at all.

So the resulting specificity is [1,1,2].

When you start using more complex CSS selector, using pseudo-classes like :not() and:is(), things get a little more complicated.

If you want to test our your own selectors, or play around with selectors to get a better feel for CSS specificity, I wrote an online calculator you can find here: CSS Specificity calculator. I’ve prefilled the selector above so you can see which parts go in which ‘bucket’.

Comparing arrays

If you have a list of selectors with specificity arrays that you want to sort, you could go compare their as first, then their bs and then their cs, and bailing when the first difference is found. Because a difference in IDs will never be overwritten by classes, and a difference in classes will never be overwritten by elements, you can return as soon as you find a difference at that level.

The sorting function for that is a little bit complex:

function compareSpecificity(a, b) {
  if(a[0] !== b[0]) {
    return b[0] - a[0];
  }
  if(a[1] !== b[1]) {
    return b[1] - a[1];
  }
  if(a[2] !== b[2]) {
    return b[2] - a[2];
  }
  return 0;
}

This will return a positive or negative number (or 0) depending on whether a or b is higher in specificity. It works, but it’s verbose. Maybe we can combine the numbers is some way to simplify our code.

The goal is to end up with a single number for each specificity that we can then compare and sort with.

The naive approach

A naive approach would be to add all three parts in a string and then parsing it to an int:

const specificityNumber = parseInt([a,b,c].join(''), 10);

While this will work in many situations, it will only work when the number of digits for a, b, and c are the same. When there’s a different number of digits, the comparison breaks. For example both [2, 0, 2] and [0, 20, 2], while being joined to "202" and "0202" respectively, will end up being 202 when cast to an int, leading to an incorrect comparison.

This approach has two issues:

  1. Leading zeroes get removed.
  2. Increasing an order of magnitude can “leak” into the higher specificity.

We need to solve both issues if we want to end up with numbers we can compare.

Preserving leading zeroes

Leading zeroes get removed because they don’t mean anything: 00010 of something and 10 of something are the same amount. So we need to make those leading zeroes significant.

The easiest way to do that is to add a digit in front of the string: 500010 is quite a different number from 510, suddenly those leading zeroes are significant.

Which digit you add doesn’t really matter, as long as you add the same one. In my function, I’m going to stick with 1:

const specificityNumber = parseInt(`1${[a,b,c].join('')}`, 10);

I’m using a template literal string (using backticks) to add a number before the specificity array. This way it won’t look like we’re trying to do calculations with strings like when we’d use "1" + [a,b,c].join('').

Take orders of magnitude into account

While we’ve solved the issue of leading zeroes, having different orders of magnitude can still cause issues:

[0,2,20] and [0,22,0] both end up being 10220 (with the prefixed 1). The second array should win because it has a higher specificity: it has 22 classes while the first just has two.

To prevent this from happening, we can make each item in the array the same length. To do that, we first need to find out what the largest number of digits in the array is:

const largestNumberOfDigits = `${Math.max(...specificityArray)}`.length;

With this bit of code we spread our array into the Math.max function, which returns the largest number in our array. Because it’s in a template literal again we can get the length of the string. With [0,2,20], the largest number is 20, and the number of characters in the string “20” is 2. So the largest number of digits is 2.

Next, we need to pad each number so it has the same amount of digits. We can do that by mapping over the specificity array, converting each number to a string and using the “padStart” function to add zeroes to the beginning:

const paddedArray = specificityArray
    .map(x => `${x}`.padStart(largestNumberOfDigits, '0')

padStart adds '0' to the beginning of the string until the string has the length of largestNumberOfDigits.

If we start with [0,2,20], the padded array will be ["00","02","20"] (notice the conversion to strings) and [0,22,0] becomes ["00","22","00"].

If we join those arrays and add the leading zero again, we end up with 10220 and 1002200 and we can see that the second specificity array wins out.

Adding it together

When we combine the three parts together, this is what you end up with:

const largestNumberOfDigits = `${Math.max(...specificityArray)}`.length;
  
const paddedArray = specificityArray
  .map(x => `${x}`.padStart(largestNumberOfDigits, '0')

const specificityNumber = parseInt(`1${paddedArray.join('')}`, 10);

combining it even further, you end up with a two-liner function:

function convertArrayToNumber(specificityArray) {
  const largestNumberOfDigits = `${Math.max(...specificityArray)}`.length;
  return parseInt(`1${specificityArray.map(x => `${x}`.padStart(highestNumberOfDigits, '0')).join('')}`, 10);
}

And here’s how to use it in a sorting function:

function sortBySpecificity(selectorA, selectorB) {
  return convertArrayToNumber(selectorB) - convertArrayToNumber(selectorA);
}

Is this more readable than the array comparison at the top of the article? You can argue both ways, if you ask me.

However, by splitting it up we can more clearly show the intent, which is not as easy by comparing different parts of an array. The function to calculate the specificity number is now separate from the comparison function, allowing for re-use elsewhere in your code.

Can this be done easier? Should I add comparisons to Polypane’s CSS Specificity Calculator? Let me know!

Hi, I'm Kilian. I make Polypane, the browser for responsive web development and design. If you're reading this site, that's probably interesting to you. Try it out!

Related Posts

New online tools: CSS specificity calculator and color contrast checker
30 March 2020, 3 minute read

My goal with Polypane is improving the workflow for developers and designers. The main focus is of course developing an excellent browser for developers and designers, but I’m also developing online tools that help out during development. Two of them online now are a CSS specificity calculator, and a color contrast checker. Both of them […]

CSS Nesting, specificity and you
4 August 2021, 9 minute read

Native CSS nesting is coming to browsers soon. With nesting, that you might be familiar with from Sass or Less, you can greatly cut down on writing repetitive selectors. But you can also really work yourself into a corner if you’re not careful. This is an overview of how you can already use it today, […]

Supercharging <input type=number>
11 August 2020, 6 minute read

The number input type provides a nice way for to deal with numbers. You can set bounds with the min and max attributes and users can press up and down to go add or remove 1, or if you add the step attribute, go up or down by a step. But what if we want […]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK