Search code examples
javascriptcss-selectorsstring-comparisoncss-specificity

Use javascript to write CSS specificity function


I need to write a javascript function, which compares two arguments passed in. They will be strings. And represent CSS classes.

The aim of the function, is to weight CSS specificity. So if an argument passed in, is a CSS ID, it should be returned rather than the second argument. (EG if a = '#id', it will beat b = '.red').

Kinda getting lost on the best approach for my javascript function. Not sure whether to use if/else statements or switch/case. Either way, it's getting messy and need some pointers.

Here's what I'm trying to do:

  • compare argument a to b, which both are strings.
  • If a css class entered is an id, (eg '#id'), this trumps all other css classes. So return this.
  • if css class is a tagname, an ID still beats this. So return the ID.
  • If '*' is entered, this has no specificity, so any other class beats this.
  • A class selector beats a tagname. (eg div.red > p).

    // '*' weakest - anything beats this
    // tagname beats '*'. EG 'div' beats '*'
    // 2 tagnames beats 1 tagname - EG 'div div' beats 'p'
    // class selector beats tagname - EG 'div.red' beats 'div'
    // id is strongest #id - '#ID' beats all the above
    
    
    function compare(a,b){
    
      const low = '*';
      const tagname = /^[a-zA-Z]+$/;
      const twoTagnames = /^[a-zA-Z\s]*$/;
    
    
      if (a === low) {
        a < b;
        return b;
      } else (b === low); {
        b < a;
        return a;
      }
    
      if (a.includes('#')) {
        a < b;
        return b;
     } else if (b.includes('#')) {
        b < a;
       return a;
     }
    
    }
    
    compare('*','#id');
    

Solution

  • I ran into this exact question recently during a job interview and I was able to figure it out like so:

    function compare(a, b) {
      let aItems = a.split([" "]);
      let aRating = [0, 0, 0];
    
      aItems.forEach((i) => {
        if (i.split("#").length > 1) {
          aRating[0] = aRating[0] + (i.split("#").length - 1);
        }
    
        if (i.split(".").length > 1) {
          aRating[1] = aRating[1] + (i.split(".").length - 1);
        }
    
        if (!i.startsWith("#") && !i.startsWith(".")) {
          aRating[2] = aRating[2] + 1;
        }
      });
    
      let bItems = b.split([" "]);
      let bRating = [0, 0, 0];
    
      bItems.forEach((i) => {
        if (i.split("#").length > 1) {
          bRating[0] = bRating[0] + (i.split("#").length - 1);
        }
    
        if (i.split(".").length > 1) {
          bRating[1] = bRating[1] + (i.split(".").length - 1);
        }
    
        if (!i.startsWith("#") && !i.startsWith(".")) {
          bRating[2] = bRating[2] + 1;
        }
      });
    
      if (aRating[0] > bRating[0]) {
        return a;
      } else if (bRating[0] > aRating[0]) {
        return b;
      }
    
      if (aRating[1] > bRating[1]) {
        return a;
      } else if (bRating[1] > aRating[1]) {
        return b;
      } else if (
        bRating[0] === aRating[0] &&
        bRating[2] === 0 &&
        aRating[2] === 0
      ) {
        return b;
      }
    
      if (aRating[2] > bRating[2]) {
        return a;
      } else if (bRating[2] > aRating[2]) {
        return b;
      }
    }
    
    

    There is one issue though, say you are comparing tagnames (ie, html, body where the more specific, the higher the specificity), then you might have some difficulty. I was able to hack my way through it string of if else tests.