How to version your UI library
Once you build a tool, you want people to use it. Several Optimizely projects were using OUI, our internal UI library, and we wanted to better answer questions from developers such as, “How do I get the latest icons?” and, “What happened to the blue drop-down menu?” We also need to easily release updates to the library without breaking anyone’s existing projects.
There are a few ways to distribute UI libraries. We opted to publish OUI on NPM and version it with Semantic Versioning. The scheme comes with a set of rules, but these rules were designed for traditional programming languages — not Sass files where most changes have visual consequences. This makes it hard to determine the severity of a change and can lead to miscategorized changes — a mistake that can have dangerous consequences.
We’ve created an interpretation of the Semantic Versioning rules at Optimizely that both adheres to the scheme, and helps mitigate the risk of incorrectly categorizing changes. Now we have guidelines to confidently and consistently determine the severity of a change that modifies the user interface. The process also introduces a new risk: breaking changes miscategorized and released as non-breaking have the potential to break Sass compilation or introduce UI bugs.
What is Semantic Versioning?
Semantic Versioning (SemVer) is a widely adopted specification that defines rules and requirements on how software should be versioned. All projects that adopt it must expose a “clear and precise” public API and update the version number based on the severity of the changes introduced.
Version numbers take the form X.Y.Z where:
- Major version (X): Must be updated if changes are not backwards compatible.
- Minor version (Y): Must be updated if changes include backwards compatible new functionality in the public API, newly deprecated APIs, or substantial new functionality/improvements to private code.
- Patch version (Z): Must be updated if changes include backwards compatible bug fixes.
How SemVer applies to Sass
All projects that implement SemVer must expose a public API. This concept, although more intuitive in other languages, also applies to Sass.
The Sass public API includes all files that can be imported along with all classes, mixins, functions, placeholders, and global variables.
In other words, the Sass public API includes (almost) everything. This is a result of the “everything is global” nature of Sass and CSS. This also makes it difficult to iterate quickly and maintain a lean codebase without introducing breaking changes.
How we classify style changes
Suppose we add a focus state to an input. Is that a breaking change, a new feature, or a backwards-compatible bug fix? What if we decrease the font-size of our tooltips? The answers are not intuitive and at Optimizely we’ve asked ourselves these questions well over 300 times.
SemVer, or any versioning specification, works well if developers consistently follow the guidelines. I created an interpretation of the SemVer specification to help achieve this consistency when classifying Sass, CSS, and visual changes as major, minor, or patch.
- Renamed or removed classes, mixins, functions, placeholders, or global variables
- Major visual changes to existing components
- Addition of a new component
- New classes, global variables, mixins, functions, or deprecated code
- Minor visual changes to existing components
- Backwards compatible Sass bug fixes
- Tiny visual changes to make the UI more consistent
Let’s revisit the previous example…
Under our guidelines, the focus state added to our input would be a patch change if it were added to achieve consistency with other UI elements. It would be a minor change if it were a new addition to the UI.
We can apply the same principles to the tooltip font-size. It would be a patch change if the font-size was changed to match other tooltips but a minor change if we just thought it looked better. It would be a major (breaking) change if we renamed the tooltip class.
The severity of a backwards compatible visual change ultimately depends on the context behind it.
Putting it to use at Optimizely
Versioning OUI has worked incredibly well for us. Thanks to NPM and Semantic Versioning, we have been able to ship UI improvements to multiple projects and allow these projects to opt-in to breaking changes.
It has worked because we follow these four principles:
- A change is considered a major change if we remove or rename code that anyone, anywhere, could potentially be using. In other words, we make no assumptions about which parts of OUI are actually being used. This is especially important since OUI is a public NPM module.
- We keep our contributor guidelines up-to-date and generate documentation for each version of OUI starting from v9.0.0.
- Every pull request must be documented in our change log. The change log helps us release versions at the correct version number and provides upgrade instructions for applications that want to upgrade to a newer version of OUI.
- We only allow one breaking change per release. Our latest policy puts the onus on the developer that introduces a breaking change to quickly update the Optimizely app to support it. It helps ensure that backwards compatible changes can always be quickly released without being blocked by unreleased breaking changes.
These principles encourage us to ship often. In under a year we’ve cut over 20 releases of OUI and are on version 11.3.0. Releasing such a large number of breaking changes is often discouraged, but it has worked well for us since OUI is primarily developed for internal use.
Adopting semantic versioning comes at a cost of increased overhead and process. Releasing a new version of OUI has seven steps — and we still find ways to improve.
Despite a few tedious steps, the process helps us work towards our goal of confidently shipping high quality UIs that look and behave consistently.