Taking CSS To Scale: A Pragmatic Approach

Updated: Jun 17, 2018



Wix’s WYSIWYG website builder supports tens of millions of users creating websites, which are accessed by hundreds of million users globally. It’s a large scale project with roughly 60 engineers collaborate on a codebase which exceeds 1.5M lines of code. Although most of these lines of code are written in JavaScript, ~130,000 lines are actually SCSS styling code that’s distributed over 1600 files. On the product aspect, the UX is complex and contains hundreds of composable building blocks for the Editor, hundreds of components for the composed websites on the Viewer side. The editor has hundreds of UI states, many different modes, customizations for each states and endless possible UI scenarios.

A Component-Based Project

Many modern web frameworks (namely AngularJS and ReactJS) assume a component structure, aiming at helping developers in creating a component abstraction. This makes the project JavaScript easy to compose, decouple, refactor and reuse. Web frameworks handle the the JavaScript of the project. What about everything else? Any web project includes CSS, which is not structured around components, doesn’t offer composability and reusability out of the box, and web frameworks do not strictly specify how to author CSS.

Unmaintainable CSS Situation

As web projects grow, CSS stylesheets tend to bloat, CSS selectors bleed and override one another, and refactoring CSS becomes very difficult. As a result, CSS hacks become the fastest way to get things done (!important, sadly). Eventually, the CSS becomes a hard-to-maintain monolith, and usually ends in a complete rewrite. Moreover, any change to the CSS code usually requires building, refreshing, and examining the result in the browser to ensure that the actual result is the expected result.

Common Approaches

OOCSS (unfit for complex apps)

Various BEM and Atomic methodologies have been around for a long time. Those methodologies require domain specific knowledge (OO for CSS, atoms, molecules, etc.) and mainly define style guides which are tailored for traditional websites and not for complex applications. These are not suited for large scale component-based projects.

INLINE STYLING (forget about reuse)

Inline styling (see ReactJS/AngularJS) would abide by the same rules as the javascript code. However, this solution doesn’t benefit from CSS reuse, and writing “CSS in JS” is just bad practice. Recommended reading on Inline Styling is James K. Nelson’s post on Why You Shouldn’t Style React Components With JavaScript.

CSS MODULES (latest hype)

The latest hype is around Glen Madden’s CSS Modules, which also mentions components and scoping. This solution, although it requires extra preprocessing and specific knowledge about the library, is a great step forward.

CSS at Scale — A Pragmatic Approach

We must keep the answer to the question “What CSS is applied to this piece of DOM?” as trivial as possible. We would like to achieve a component-based CSS structure so it would be just as reusable, composable, decoupled and easy to refactor, as our javascript code.

  • Composable - Reuse any component and its CSS will “just work”.

  • Easy to Refactor - Reduce CSS fragility, be aware of the affected scope when making changes.

  • Decoupled - Keep changes contained within a single component.

  • Reusable - Use a component multiple times within different parent components.

6 Guidelines For A Successful CSS Architecture

In order to implement this pragmatic approach, we’ll take advantage of SCSS and some of its built-in features.

1) DOM & CSS Go Together

Always place CSS code right next to where its DOM is declared or generated. Whether in a JS, HTML, SVG, HTML or any other template solution of choice, a corresponding .scss file should follow. Thus, avoid having a dedicated /style or /css folder for the CSS.

This significantly eases future refactoring and enables other developers to easily locate all of the component’s related code.

For example, this is a basic component folder structure:


2) Namespaced Components

Each component must have a single root DOM node, with a unique class name at the project’s level. The same class name should be solely scoping (nesting) the entire corresponding scss file, utilizing SCSS’s nesting feature. This method eases future refactorings: when changes are made - the effects are guaranteed to be scoped. It also enables reusability: when one component is composed in another, it is certain that their CSS rules won’t override. Simple example:

  • HTML: <div class="my-calendar">...</div>

  • SCSS: .my-calendar { ... }

Unique namespacing can be achieved by following a strict convention (e.g pacomo), or by using a dedicated tool, such as a grunt task, to enforce uniqueness and warn at build time when there’s a namespace conflict in the project’s SCSS files.

3) Non-Bleeding CSS

Bleeding occurs when CSS rules undesirably affect other pieces of DOM, causing side effects. This happens when selectors affect an unknown or unspecified child node. Avoiding CSS bleeding is relatively easy to achieve by only using selectors which affect known children, like so: Always prefer an immediate child selector over a regular descendent selector:



Avoid universal selector, which selects as a wildcard of everything on your DOM and looks like this:




Don’t use it. Instead, specify exactly which elements should be selected, like so:



This ensures composability and reusability, since it prevents CSS from bleeding into nested child components, which may internally use .child-class.

4) Strict SCSS File Structure

As many developers collaborate to the same code, keeping the SCSS file structure neat and easy to read has clear benefits. A developer should be able to glance at the file, and trivially know what CSS is applied to a certain piece of corresponding HTML.

Here’s how it’s done: The SCSS file structure should match the DOM structure; Each section of CSS should be nested just as it is nested in the DOM hierarchy. Each CSS section should be organized: Own style rules first, then modes, then children. This can be enforced using scss-lint, by enabling the DeclarationOrder, and MergableSelector linters. Here’s an example SCSS declaration:



5) In Place Overrides

CSS overrides are customizations to the component’s CSS whenever it’s nested within parent components. They should be defined alongside the component’s CSS and not anywhere else. Avoid styling overrides in the parent component’s style. Use the built-in SCSS parent selector:





6) Component Modes

Component Modes are different CSS style rules of the same component, which are applied when an additional class name is authored on the component’s root node. This effectively declares the component’s API, with regard to styling. For example, consider a Message component, which has two modes: success and error:





Authoring Mode-Specific CSS to Deep Child Nodes

Trying to modify the CSS of some deep DOM nodes of the component becomes a bit more tricky. We basically want to express: “When the root node has a mode class, put some different CSS on a child node”.

In the following example, we want .a-second-child to be color: black, but when .message has the .error mode, .a-second-child should be styled color: red. Following guideline (4) above with regard to the file structure, the SCSS hierarchy should match the DOM hierarchy:

HTML: ```html

....




Using the parent selector & doesn’t help here, because it sets .error class as an ancestor of .message, rather than as a mode - as an additional class. The desired CSS should actually be without a space, meaning both .error and .message are authored on the same element:





Using Mixins

Similarly, we can reflect changes due to modes for children with any type of customization, with a full library of mixins:

  • when-root-has-class()

  • when-root-has-all-classes()

  • when-root-has-any-class()

  • when-root-has-pseudo-class()

  • when-root-has-all-pseudo-classes()

  • when-root-has-any-pseudo-classes()

  • when-root-has-attribute()

  • when-root-has-all-attributes()

  • when-root-has-any-attribute()

See a full example of a custom checkbox implementation and the full library documentation.

Mixins help improving the CSS code:

  • Makes the SCSS code very DRY.

  • Puts all CSS relevant to a certain DOM node in one place.

  • Eases refactoring and improves readability.

  • Eases reuse of CSS and avoiding redundant overrides.

Conclusion

As any component is aware of its DOM, CSS should go hand in hand with that DOM. By the guidelines above, CSS becomes composable, easily refactored, decoupled and the components are reusable out of the box, with regard to styling.

Takeaways

  • No assumptions made on which template language is used for creating or generating DOM.

  • All CSS which affects a certain piece of DOM is always placed next to it in a dedicated SCSS file, so it’s easy to find and change. The answer to “What CSS is applied to a piece of DOM?” is very clear.

  • Unifying similar style rules becomes trivial; It’s all in the same scss file.

  • All component style rules are nested in its selector. Nothing is at the global scope.

  • When refactoring a component (changing or deleting), it is easy to see where all its CSS is.

  • Easy to reuse an existing selector.

  • Easy to write CSS without opening a browser.

Thanks to Shai Kfir, Marlowe Shaeffer, Karen Cohen and Sharon Rousso for their help in writing this article.


This post was written by Eitan Rousso

#SCALE #CSS #architecture

152 views
  • Black Twitter Icon
  • Black YouTube Icon

At Wix Engineering we develop some of the most innovative cloud-based web applications that influence our +180 million users worldwide.

Have any questions?
Email: wixeng@wix.com

Trademarks and logos of other parties appearing in this post are the property of their respective holders.

Get Wix Engineering Straight to Your e-mail