CSS Specificity Graph in a Fractal Styleguide

Published on

A specificity graph shows how specific selectors are in a CSS file. It’s a type of graph that should be considered conceptual, to give an idea of the overall quality of the code rather than try produce a perfect codebase. The idea came from Harry Roberts in 2014 and later a tool was created by Jonas Ohlsson.

I’ve always tried to write CSS that isn’t too specific and so in the past I have used these graphs to see the state of a codebase. It wasn’t until this week however that I decided to automate it. I wanted a way for the whole team to see the state of the CSS file outputted by Sass. It is very easy to write Sass that produces highly specific CSS and so having a graph that shows an easily glanceable overview of the output will hopefully be useful.

I’m going to explain exactly how I used Jonas’ tool to create an automated graph but first I must ensure you understand the following: CSS should generally be as least specific as possible, but there are plenty of cases where spikes in the graph are completely fine. Aiming for a perfect graph will probably create far more messy CSS than if it had some spikes.

Specificity Graph

Our team is already using Fractal as a style guide, specifically for being able to create components that can easily be viewed as an individual or within a layout. The way Fractal is set up by default gives a folder for documentation and components, so I thought it would be nice to have the graph as part of the documentation. These files are generally markdown files but you’re able to run code inside it due to the way the files are parsed. After some fiddling around, I came to the conclusion that the best way to do it would be to have the graph generated as a hidden component that gets rendered within a markdown file. It was hidden by simply adding a _ to the beginning of the folder that contained the component (which I called _cssgraph/).

We already use gulp so I started by installing Jonas’ tool with npm install specificity-graph then created a task in the gulpfile. There were a few issues that I came across, I considered rewriting the tool to fit with our requirements but decided for now to just make it work. So the task lint:styles-graph runs the specificity graph tool and outputs to src/codebase/components/_cssgraph/. Instead of using the generated HTML, I decided to go the route of putting the contents of the json file into a variable that could then be rendered by the component.


gulp.task('lint:styles-graph', function () {
    const file = fs.readFileSync("css/style.css", "utf8");
    const path = `${__dirname}/src/codebase/components/_cssgraph`;
    specificityGraph(path, file, function (directory) {
      // Add the contents of specificty.json to a variable in specificity.js 
      fs.readFile(`${path}/specificity.json`, 'utf8', function (err, data) {
        if (err) return console.log("ERROR: " + err)
        var newData = "var embeddedJsonData = " + data;
        fs.writeFile(`${path}/specificity.js`, newData)
      })
    })
})

We already use nunjuks for templating but you could use handlebars. The cssgraph.njk file is the component itself, which is mostly a copy from the index.html that was originally created. This gives you full control over how the graph looks and any HTML that is added to the style guide. I committed the cssgraph.njk file and the markdown file to git, the rest is automatically generated by the gulp task.


<style>
   * {
     padding: 0; margin: 0;
     box-sizing: border-box;
   }
   body {
     font-size: 100%;
     font-family: 'Helvetica', sans-serif;
     padding-top: 2em;
     padding-bottom: 1em;
   }
   small {
     font-size: 0.8em;
   }
   .abs-top-right {
     position: absolute;
     top: 0;
     right: 0;
   }
   .center-wrapper {
     text-align: center;
   }
   .main-header {
     padding: 0 0.5em;
   }
     .main-title {
       font-family: 'Gill sans', sans-serif;
       margin-bottom: 4px;
     }
     .main-info {
       color: #606060;
     }
     .main-header-link {
       text-decoration: none;
       color: #999;
     }
 
   .svg-container {
     margin-bottom: 1em;
   }
   @media(min-width: 1000px){
     .svg-container {
       width: 1000px;
       margin-left: auto;
       margin-right: auto;
       margin-bottom: 2em;
     }
   }
   .padding-height {
     width: 100%;
     height: 0;
     padding-top: 40%;
     position: relative;
   }
 
   .vis {
     position: absolute;
     left: 0;
     top: 0;
     right: 0;
     bottom: 0;
     display: block;
     font-family: sans-serif;
     font-size: 1.6em;
   }
   @media(min-width: 500px){
     .vis {
       font-size: 1.3em;
     }
   }
   @media(min-width: 700px){
     .vis {
       font-size: 0.9em;
     }
   }
     .vis path {
       fill: none;
       stroke: steelblue;
       stroke-width: 4px;
     }
 
     .vis .axis {
       opacity: 0.6;
     }
       .vis .axis .tick {
         display: none;
       }
       .vis .axis--show-ticks .tick {
         display: block;
       }
 
     .vis .domain-label {
       font-size: 0.9em;
       fill: steelblue;
     }
 
 
     .vis .overlay {
       fill: none;
       pointer-events: all;
     }
     .vis .focus {
       position: relative;
     }
       .vis .focus-text-background {
         fill: #333;
       }
         .vis .focus-text {
           font-size: 0.9em;
           fill: #eee;
         }
 
       .vis .focus circle {
         fill: none;
         stroke-width: 2.5px;
         stroke: #0c0;
       }
 
     @media(min-width: 600px){
       .mobile-focus-controls {
         display: none;
       }
     }
 
     button {
       display: inline-block;
       width: 50%;
       text-align: center;
       background: #e0e0e0;
       border: 0;
       border-right: 1px solid white;
       padding: 1em 0;
       margin-bottom: 1em;
     }
 </style>
 
 <div class='svg-container'>
   <div class='padding-height'>
     <svg class='vis js-graph' viewBox='0 0 1000 400'>
     </svg>
   </div>
 </div>
 
 <div class='mobile-focus-controls'>
   <button type="button" class="js-prev">Previous</button><button type="button" class="js-next">Next</button>
 </div>
 
 <script src="/components/raw/cssgraph/specificity.js"></script>
 <script src="/components/raw/cssgraph/specificity-graph-standalone.js"></script>
 <script src="/components/raw/cssgraph/example.js"></script>

This leaves us with just the markdown file left, which is used to render the component in the documentation. You can put this wherever you would like.

<h1>CSS Specificity Graph</h1>
<p>This graph is a conceptual visualisation of the outputted CSS, it does not have to be perfect but gives an idea of where the Sass may be producing over-specified CSS.</p>
<p>Spikes are "bad", and the general trend should be towards higher specificity later in the stylesheet. <a href="https://csswizardry.com/2014/10/the-specificity-graph/">More info</a></p>
{% render '@cssgraph' %}

And that’s all there is to it. The gulp task gets run on watch, so whenever a Sass file changes it gets built to CSS and a graph is outputted to the documentation. This makes it easy to visualise the generated CSS as changes are made, and to see the state of the codebase in general.