At New Relic, the user-interface infrastructure team delivers components for the rest of the company to use. These components are shipped as a single NPM package, providing both the JavaScript and Syntactically Awesome Style Sheets (SASS) that New Relic teams can reference via their package.json file.

Because our products are built of multiple pieces, we could occasionally end up with a single page built using different internal NPM packages, and these packages may depend on different versions of the New Relic UI. The styles of the latest included version would then override styles of any other versions in the page.

Conflicts that can arise in JavaScript are often easily solved, thanks to the availability of tooling like NPM; but CSS (Cascading Style Sheets) has a single global context and lacks the ability to contextualize. We had to come up with a way of fixing this for the New Relic teams that use the components.

There is no perfect solution for CSS versioning issues—basically, double-class collisions in CSS—and the industry approaches vary widely, from a single, global CSS that prevents double inclusions to approaches that involve writing all CSS in JavaScript. Each approach has benefits and drawbacks, but we wanted to share how we solve the problem in hopes that our solutions can inform your own efforts to deal with CSS conflicts.

We had three key requirements for our solution:

  1. Standard-following: The accepted standard for CSS company-wide is to use SASS, so our versioning solution should also follow it. In addition, we wanted to take advantage of the robust tooling around CSS/SASS.
  2. Extensibility and override-ability: Although the goal of our UI component package is to standardize behavior and look-and-feel across all our products, the transition must be made in small bits. Therefore, teams may need the ability to override individual styles if a component does not fit their needs.
  3. Zero overhead in production code: We wanted to minimize the impact on runtime, so solutions that involved calling several JavaScript functions for each node to be styled didn’t please us.

Solution development

With these requirements in mind, we iterated different solutions. Let’s walk through some code samples to illustrate our approach.

This is what our CSS looks like:

.nr-table {
   overflow-y: scroll;
 
   &--dark {
      background: #333;
   }
 
   &-cell {
      display: flex;
      height: 2em;
   }
 
   // More rules...
}

The first solution consisted in wrapping all root classes in SASS with an extra class. Then, the root element of each component will include these classes. For the example above, it would look like this:

.v5-0-1 {
   // Note that rules applied to the root element are
   // wrapped in a different selector ("&.#{$root}").
   &.nr-table {
      overflow-y: scroll;
 
      &--dark {
         background: #333;
      }
   }
 
   // Rules applied to child elements are kept as-is.
   .nr-table {
      &-cell {
         display: flex;
         height: 2em;
      }
   }
}

This will output selectors like:

.v5-0-1.nr-table { ... }
.v5-0-1.nr-table--dark { ... }
.v5-0-1 .nr-table-cell { ... }

With these selectors you can use—together with the main class of your root element (nr-table in the example)—the version class (v5-0-1):

<div class="v5-0-1 nr-table">
   <div class="nr-table-cell">
      <!-- Content... -->
   </div>
</div>

We had planned to add the root class in SASS via a helper and in JavaScript through a common base class in all our components. This would have implied minimal code changes. However, we soon found problems with this approach.

Many of our components can accept other subcomponents, such as a link or button, as children. In the example we use, a link could be inserted in one table cell. But this creates the possibility of a collision between the table version and the link version:

<div class="v5-0-1 nr-table">
   <div class="nr-table-cell">
      <a href="/path/to/page" class="v4-3-1 nr-link">
         <span class="nr-link-content">Fancy external page</span>
      </a>
   </div>
</div>

In the HTML above, the nr-link-content matches with version 4.3.1 as expected, but also with 5.0.1 (i.e., selector .v5-0-1) because of the table version. This collision breaks the version sandboxing we wanted to achieve, thus making the solution unusable.

Fixing CSS double-class collisions

To fix this issue, we decided to prefix each class with the version. This approach doesn’t impact the specificity of the selector. However, the process of adding the version to each class becomes too complex to be done manually, so we decided to automate it.

First, we picked a unique prefix. All classes already start with nr-, but we thought that was too generic, so we changed it. In our case we chose nr-css-. Then, we looked through all the UI infrastructure codebase to make sure this combination was not used anywhere else. Finally, we clearly stated that nr-css- implicitly meant a class: All classes must start with nr-css, and you can use nr-css only if you are talking about CSS classes.

Next, we developed two Webpack loaders that do AST (Abstract Syntax Tree) traversing (using Gonzales-Pe for SASS, and Babylon + Recast for JavaScript) to find all Literals and TemplateLiterals containing nr-css and modify them by prepending the version (e.g., nr-css-link-content becomes v4-3-1-nr-css-link-selector). Webpack loaders are transformations applied on a resource file of your app. They are functions (running in Node.js) that take the source of a resource file as the parameter and return the new source.

Although it might seem complex, both loaders have less than 60 lines each and were written in approximately one day.

Fine-tuning overrides

As stated above, sometimes temporary overrides are necessary while adopting components on older UIs because of look-and-feel incompatibilities.

So our end solution slightly modifies how the version is prepended to each class. We extract version numbers, reverse them, and transform them into a two-digit letter code (the number in base 26, using only letters). For example, version 2.9.114 gets transformed into EKAJAC (AC for 2, AJ for 9 and EK for 114).

This allows fine-tuning selection for overrides. In theory, no user of the library should override styles. However, the adoption process is not instantaneous, and some inconsistencies may arise, so we give the possibility of overriding while introducing some intentional friction in the process. The version “hash” plays an important role on how to define your override:

Let’s imagine we want to override something in nr-css-table-cell. Its full CSS selector for version 2.9.114 is then EKAJAC-nr-css-table-cell. Depending on how much you select from it (via CSS attribute selectors), you can decide version overriding:

EKAJAC-nr-css-table-cell example

The UI Infrastructure team also provides a SASS helper that can combine the desired override into a combined attribute selector. For example, for overriding all patches in version 2.9 (i.e., ~2.9), the consumer would write:

@include nr-css-override("table-cell", 2, 9) {
   background: red;
}

Which would then compile into CSS as:

[class*="AJAC-nr-css-table-cell "], [class$="AJAC-nr-css-table-cell"] {
   background: red;
}

Our solution to CSS versioning issues

We decided to use the solution that had minimal impact in the current codebase, as well as a negligible impact in performance. In the final approach, zero additional lines of JavaScript are executed on each render.

We also provide a fine-grained way of overriding custom rules to ease the transition while adopting the library. This also lets us decide to disable these rules in the future, instantly making all UIs consistent. Again, we hope that this look into our solution can help address any CSS versioning issues you may face.

Former New Relic Lead Software Engineer Miguel Jiménez Esún contributed to this post.

 

Javier Sánchez-Marín is a lead software engineer for New Relic in Barcelona, Spain.

View posts by .

Interested in writing for New Relic Blog? Send us a pitch!