How we changed the way we style our UI
In the last couple of years we’ve been improving the way we do UI development at Unbounce. We started by hiring people specifically dedicated to champion UI development who we’ve embedded in each squad, and we’re constantly looking at ways to improve our workflow.
A couple of months ago, we also started using React. And with this came a new opportunity to redefine the way we’re styling our interface at Unbounce. While we’re constantly challenging and evolving what we consider best practice, I thought I’d share some of the new tools and methodologies that have helped us ship user interfaces faster and more consistently, also on how to alleviate some of the long standing css pains we had in the past like:
- CSS Global Variables
- How to refactor in a safe way
- CSS Dependency Management
- Dead Code Elimination
- Shared Variables
- Browser Compatibility
CSS Global Namespace
Best practices in any other language would tell us to avoid using global variables. Still, in CSS everything is a global variable, which makes it really easy for styles to leak and it can be a problem trying to override existing styles. We got to a point where the only option we had was to keep adding code and not being able to remove complexity or reuse some classes. When a single developer is working on a project, name collisions are easier to avoid and manage though still problematic. However with more than 40 developers shipping code every day, it’s impossible to keep track of component name dependencies, and changing a single class may lead to dozens of unintended or uncaught consequences on affected pages.
When we migrated to React, we started breaking things into components. Components provide a way to write small parts of code that can be easily used as part of a larger screen, application or system. Each component has a particular style associated with it, and no matter the context where the component is being rendered, it should always look the same.
For years developers have been trying to fix this problem. Methodologies and conventions like BEM, OOCSS, and SMACSS have offered a huge improvement on the modularity, logic and structure of CSS. But it is far from ideal, developers still have to deal with uniquely naming things, extremely long names, navigating through hundreds of lines of CSS to find the right place to modify styles, and of course the fact that all CSS classes are still global.
Truth is, CSS doesn’t allow us to scope our styles to a particular component in an HTML document. But in May, 2015 things changed when css-modules made its first appearance.
Thanks to tools like Webpack, Browserify and JSPM, we’re expected to write code in small modules, each one encapsulating their explicit dependencies, css-modules is no different. Now we can rely on Webpack to import our CSS from within a JavaScript module. Using a feature known as local scope from css-loaders which allows us to export class names from our CSS into the JavaScript code.
Let me explain what I mean. Let’s build a simple button component. For each component we’ll create a JS file and a CSS file. We name both files the same way so that they show up next to each other on our components folder, and to eliminate the confusion on where to modify the styles for a particular component.
// Button.css .wrapper { padding: 5px 10px; border-radius: 3px; } .icon { width: 50px; height: 50px; display: inline-block; background-repeat: no-repeat; } .text { margin-left: 20px; } // Button.jsx import styles from ‘./Button.css’; const Button = (content) => { return( <div className={ styles.wrapper }> <div className={ styles.icon }></div> <div className={ styles.text}>{ content }</div> </div> ) } export default Button;
So far, we’ve defined our styles on our css file, yes the classes can be as generic as you want, you can use .button, .title, .wrapper… We’re thinking locally remember? Then we import that stylesheet into a component and assign it to a variable named style. From now on, all our styles are available from that variable as if they were a javascript object.
I bet you’re wondering how are we going to prevent such generic classes from leaking out of our component as our codebase gets bigger. Well that’s the magic behind css-modules, since webpack is importing the CSS in and out of JS, and it’s handling the views at the same time, we have the ability to make these changes on the class names where we don’t with either vanilla CSS or pre processors (because we don’t have knowledge of the views). On the css-loader configuration you can specify what you want your classes to look like based on the component or folder, class name, file path , and of course a random hash to ensure your class doesn’t exist on any other place in the app. You can get as creative as you want, and we usually have two different build configurations, one for development and one for production. So the output CSS of our previous example (in a development environment) would be as follows:
//bundle.css .Button_wapper__1xkp5 { padding: 5px 10px; border-radius: 3px; } .Button_icon__78khj { width: 50px; height: 50px; display: inline-block; background-repeat: no-repeat; } .Button_text_879fd { margin-left: 20px; }
Our production environment code would look more like this:
//bundle.css .1xkp5 { padding: 5px 10px; border-radius: 3px; } .78khj { width: 50px; height: 50px; display: inline-block; background-repeat: no-repeat; } .879fd { margin-left: 20px; }
Cool right? This way it’s really easy to understand where to specify the changes for a particular part of the app and it makes it super easy for development, without having to worry about your styles leaking to any other part of the code. Also, it’s so nice to have stylesheets that are rarely longer than 50 lines.
When you enable css-modules on your Webpack’s css-loader configuration, locally scoped CSS is enabled by default. But you can switch it off with :global(...)
or :global
for selectors and/or rules you wish to keep in the global namespace. So for example if we need to have a global classname available we would write it the following way:
:global( .globalClass ) { width: 50px; height: 50px; display: inline-block; } .wrapper { padding: 5px 10px; border-radius: 3px; } .wrapper :global( .link ) { text-decoration: none; }
And the output of this file would be something like this:
.globalClass { width: 50px; height: 50px; display: inline-block; } .1kxp4 { padding: 5px 10px; border-radius: 3px; } .1kxp4 .link { text-decoration: none; }
Refactoring and Dead Code Elimination
Migrating towards this approach, is not something that can happen overnight, we’re working towards building a team that builds core components (such as buttons, dropdowns, images, etc…) and replacing them in our codebase. Once we fully migrate global stylesheets to local styling, refactoring components and deleting dead code or other static assets like images will be very straightforward. If you wish to modify a component there’s only one place where you need to do the changes, and they will be reflected everywhere in the app. And if you have to delete a component, you can just delete it all together, without worrying about any dependencies that the styles for that particular component may have on any others.
Shared Variables and Browser Compatibility
Having shared variables is one of the most basic features of any programming language. Some years ago CSS preprocessors like Sass or LESS changed the way we think of CSS. A CSS preprocessor allows you to write CSS in a different language, and compiles it to plain old CSS, they introduced a lot of functionality from other programming languages such as variables, functions, loops and conditionals making it easier to write reusable, maintainable and extensible codes in CSS. We’ve used Sass for a long time, but recently we’ve recently adopted using PostCSS, a new tool that has provided great benefits and flexibility on the web-styling front.
Before I get all excited talking about it, let me clarify what I mean by PostCSS.
- PostCSS the tool itself, is a Node.js module that parses CSS into an abstract syntax tree, passes that AST through any number of “plugins” and then converts it back into a string, so you can output it to a file.
- The PostCSS plugin ecosystem refers to all the plugins built with PostCSS in mind, they can do pretty much whatever they want with the parsed CSS.
PostCSS provides the benefits of pre-processors and more, thanks to their simple and straightforward API that developers can use to write custom plugins. The best part, is that it doesn’t clash with pre-processors you can use them together with no problem.
The reason we decided to start moving away from preprocessors is because we realized they were encouraging some anti patterns like nesting too many times or inconsistencies both on the way we write and organize code. The problem with nesting is that best practices for css say that the fewer rules required to check for a given element, the faster the style resolution will be. For the longest time I thought that the browsers read CSS from left to right (and I think a lot people do too). So, if I wrote the following selector #main-nav .menu a
it would take the id main-navigation and then look for the class menu, and then target all the anchor tags. Well, it turns out I was wrong. The browsers read CSS from right to left, so in this case, it would take all of the anchor tags on the page, then filter the ones that are nested on a menu class, and then just apply to the ones that are nested on a tag with the id ‘main-nav’. Preprocessors make it so easy to use nesting as a way to keep things local, that we were shooting ourselves in the foot performance wise, by having to many rules for a particular element.
To make up for great preprocessor features like variables, we’re using PostCSS Simple Vars, which allows you to define a JS object of variables that can be used across all of your stylesheet.
Another problem we were having (specially with cross browser compatibility) is that each developer was writing css property prefixes differently, or not using them at all. So it was hard to maintain consistency on the way we were writing code. PostCSS Autoprefixer has been incredibly usefull in this front by automatically adding vendor prefixes to CSS rules using values from Can I Use.
Adding linters, encouraging code reviews and keeping coding style guides has helped us improve the consistency in the way we write code.
Just like any other aspect of software development, improving the way we build, maintain and scale our UI is an iterative process. We noticed some pains we were having like refactoring code, dead code elimination, CSS global namespace, shared variables and browser compatibility and we have come across some great tools like webpack, css-modules from css-loaders, PostCSS and their plugin ecosystem like Autoprefixer and Simple-vars that are helping us improve on shipping user interfaces faster and more consistently. We might find out a better approach to what we’re doing, but that’s what makes our department so amazing, we are always evaluating better ways of working, and better tools to use. We’re at a point in most of our teams where we can introduce new methodologies if they provide value.We’re not afraid to try new things and encourage failure, because we truly believe that’s the best way to learn.