├── .gitignore ├── README.md ├── box-model.png ├── give-up.gif └── quick-open-file.png /.gitignore: -------------------------------------------------------------------------------- 1 | /hours.txt 2 | /.idea 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 8 simple rules for a robust, scalable CSS architecture 2 | 3 | ### Translations 4 | 5 | - [Português (Brasil)](https://medium.com/tableless/8-regras-simples-para-uma-arquitetura-css-robusta-e-escal%C3%A1vel-545c6dade170) 6 | - [Chinese](http://www.jianshu.com/p/acb4b9d8ff4f) 7 | 8 | This is the manifest of things I've learned about managing CSS in large, complex web projects during my many years of professional web development. I've been asked about these things enough times that having a document to point to sounded like a good idea. 9 | 10 | I've tried to keep the explanations short, but this is essentially the tl;dr: 11 | 12 | 1. [**Always prefer classes**](#1-always-prefer-classes) 13 | 1. [**Co-locate component code**](#2-co-locate-component-code) 14 | 1. [**Use consistent class namespacing**](#3-use-consistent-class-namespacing) 15 | 1. [**Maintain a strict mapping between namespaces and filenames**](#4-maintain-a-strict-mapping-between-namespaces-and-filenames) 16 | 1. [**Prevent leaking styles outside the component**](#5-prevent-leaking-styles-outside-the-component) 17 | 1. [**Prevent leaking styles inside the component**](#6-prevent-leaking-styles-inside-the-component) 18 | 1. [**Respect component boundaries**](#7-respect-component-boundaries) 19 | 1. [**Integrate external styles loosely**](#8-integrate-external-styles-loosely) 20 | 21 | ## Introduction 22 | 23 | If you're working with frontend applications, eventually you'll need to style things. And even though the state-of-the-art of frontend applications keeps blazing ahead, CSS is still the only way to style anything on the web (and lately, in some cases, [native applications too](https://facebook.github.io/react-native/)). There's two broad categories of styling solutions out there, namely: 24 | 25 | * CSS preprocessors, which have been around for ages (such as [SASS](http://sass-lang.com/), [LESS](http://lesscss.org/), and others) 26 | * CSS-in-JS libraries, which are a relatively new approach to styling (such as [free-style](https://github.com/blakeembrey/free-style), and [many others](https://github.com/MicheleBertoli/css-in-js)) 27 | 28 | The choice between the two approaches is a topic for a separate article, and as usual, both have their pros and cons. That said, I'll be focusing on the former approach, and if you've chosen to go with the latter, this article will probably be a bit less interesting. 29 | 30 | ## High-level goals 31 | 32 | So we're after a robust, scalable CSS architecture. But what properties does that call for, specifically? 33 | 34 | * **Component oriented** - The best way to deal with UI complexity is to split the UI into smaller components. If you're using a sensible framework, the JavaScript side of this will come naturally. [React](https://facebook.github.io/react/), for instance, encourages a high-level of componentization and compartmentalization. We want a CSS architecture to match. 35 | * **Sandboxed** - Splitting the UI into components won't help our cognitive load if touching the styles of one component can have unwanted and unpredictable effects on another. Fundamental CSS features such as the [cascade](https://developer.mozilla.org/en/docs/Web/Guide/CSS/Getting_started/Cascading_and_inheritance), and a single, global namespace for identifiers actively work against you in this regard. If you're familiar with the [Web Components spec](https://developer.mozilla.org/en-US/docs/Web/Web_Components), think of this as getting the [style isolation benefits of the Shadow DOM](http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-201/) without having to care about browser support (or whether or not the spec ever gets serious traction). 36 | * **Convenient** - We want all the nice things, and we don't want to work for them. That is, we don't want to make our developer experience any worse by adopting this architecture. If possible, we want to make it better. 37 | * **Err on the side of safety** - Somewhat related to the previous point, we want everything to be *local by default*, and global only as an exception. We engineers are lazy people, and the path of least resistance always needs to point to the correct solution. 38 | 39 | ## Concrete rules 40 | 41 | ### 1. Always prefer classes 42 | 43 | This is just to get the obvious out of the way. 44 | 45 | Do not target ID's (e.g. `#header`), because whenever you think there can be only one instance of something, [on an infinite timescale](https://twitter.com/stedwick/status/525777867146539009), you'll be proven wrong. One past example of this was when we wanted to weed out any data-binding bugs on a large application we were working on. We started two instances of our UI code, side-by-side in the same DOM, both bound to a *shared* instance of our data model. This was to make sure that all changes in the data model were correctly reflected in both UI's. Any components that you might have assumed to always be unique, such as a header bar, no longer are. This is a great benchmark for surfacing other subtle bugs related to assumptions about uniqueness too, by the way. I digress, but the moral of the story is: there's no situation where targeting an ID would be a *better* idea than targeting a class, so let's just not, ever. 46 | 47 | Neither should you target elements (e.g. `p`) directly. It's often OK to target elements that *belong to a component* (see below), but on their own, eventually you'll end up having to [undo those styles](http://csswizardry.com/2012/11/code-smells-in-css/) for a component that doesn't want them. Recalling our high-level goals, this also goes against just about all of them (component-orientedness, avoiding the cascade like the plague, and being local by default). Setting things like fonts, line-heights and colors (a.k.a [inherited properties](https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance)) on `body` *can* be the exception to this rule if you so choose, but if you're serious about component isolation, it's completely feasible to forgo even these (see below about [working with external styles](#8-integrate-external-styles-loosely)). 48 | 49 | So with very few exceptions, your styles should always target a class. 50 | 51 | ### 2. Co-locate component code 52 | 53 | When working on a component, it helps tremendously if everything related to that component — its JavaScript, styles, tests, documentation, etc — live very close to each other: 54 | 55 | ``` 56 | ui/ 57 | ├── layout/ 58 | | ├── Header.js // component code 59 | | ├── Header.scss // component styles 60 | | ├── Header.spec.js // component-specific unit tests 61 | | └── Header.fixtures.json // any mock data the component tests might need 62 | ├── utils/ 63 | | ├── Button.md // usage documentation for the component 64 | | ├── Button.js // ...and so on, you get the idea 65 | | └── Button.scss 66 | ``` 67 | 68 | When you're working in the code, simply open your project browser, and all other aspects of the component are at your fingertips. There's a natural coupling between the styles and the JavaScript that produces your DOM, and it's a fair bet you'll be touching one soon after touching the other. The same applies to a component and its tests, for example. Think of this as the [locality of reference principle](https://en.wikipedia.org/wiki/Locality_of_reference) for UI components. I, too, used to meticulously maintain separate mirrors of my source tree under `styles/`, `tests/`, `docs/` etc, until I realized that literally the only reason I kept doing that was because that's how I'd always done it. 69 | 70 | ### 3. Use consistent class namespacing 71 | 72 | CSS has a single, flat namespace for class names and other identifiers (such as ID's, animation names, etc). Just like in the PHP days of old, the community has dealt with this by simply using longer, structured names, thus emulating namespaces ([BEM](http://getbem.com/) is an example). We'll want to choose a namespacing convention, and stick with it. 73 | 74 | For instance, let's say we use `myapp-Header-link` as a class name. Each of its 3 parts have a specific function: 75 | 76 | * `myapp` to first isolate our app from other apps possibly running on the same DOM 77 | * `Header` to isolate the component from other components in the app 78 | * `link` to reserve a local name (within the component's namespace) for our local styling purposes 79 | 80 | As a special case, the root element of the `Header` component can be simply marked with the `myapp-Header` class. For a very simple component, that might be all you need. 81 | 82 | Whatever namespacing convention we choose, we'll want to be consistent about it. In addition to each of the 3 parts having a specific *function*, they'll also have a specific *meaning*. Just by looking at a class, you'll know where it belongs. The namespacing will be the map by which we navigate the styles of our project. 83 | 84 | From now on I'll assume the namespacing scheme of `app-Component-class`, which I've personally found to work really well, but you can of course also come up with your own. 85 | 86 | ### 4. Maintain a strict mapping between namespaces and filenames 87 | 88 | This is just the logical combination of the preceding two rules (co-locating component code, and class namespacing): all styles affecting a specific component should go to a file named after the component. No exceptions. 89 | 90 | If you're working in the browser, and you spot a component that's misbehaving, you can right-click-Inspect it, and you'll see for instance: 91 | 92 | ```html 93 |
...
94 | ``` 95 | 96 | Noting the name of the component you switch to your editor, hit the key combo for "Quick open file", start typing "head", and there you go: 97 | 98 | ![Quick open file](quick-open-file.png) 99 | 100 | This strict mapping from UI components to the corresponding source files is doubly useful if you're new on the team and don't know the architecture by heart yet: you don't need to, to be able to find the guts of the thing you're supposed to work on. 101 | 102 | There's a natural (but perhaps not immediately obvious) corollary to this: a single style file should only contain styles belonging to a single namespace. Why? Say we have a login form, that's only used within the `Header` component. On the JavaScript side, it's defined as a helper component within `Header.js`, and not exported anywhere. It might be tempting to declare a class name `myapp-LoginForm`, and sneak that into both `Header.js` and `Header.scss`. But let's say the new guy on the team is be tasked to fix a small layout issue in the login form, and inspects the element to figure out where to start. There is no `LoginForm.js` or `LoginForm.scss` to be found, and he has to resort to `grep` or guesswork to find the relevant source files. That is to say, if the login form warrants a separate namespace, split it into a separate component. Consistency is worth its weight in gold in projects of non-trivial size. 103 | 104 | ### 5. Prevent leaking styles outside the component 105 | 106 | So we've established our namespacing conventions, and now want to use them to sandbox our UI components. If every component only uses class names prefixed with their unique namespace, we can be sure that their styles never leak to their neighbors. This is very effective (see below for the caveats), but having to type the namespace over and over again also gets rather tedious. 107 | 108 | A robust, yet still tremendously simple solution to this is to wrap the entire style file into a prefix block. Note how we only have to repeat the app and component names once: 109 | 110 | ```scss 111 | .myapp-Header { 112 | background: black; 113 | color: white; 114 | 115 | &-link { 116 | color: blue; 117 | } 118 | 119 | &-signup { 120 | border: 1px solid gray; 121 | } 122 | } 123 | ``` 124 | 125 | The above example is in SASS, but the `&` symbol — perhaps shockingly — works the same across all relevant CSS preprocessors ([SASS](http://sass-lang.com/), [PostCSS](https://github.com/postcss/postcss-nested), [LESS](http://lesscss.org/) and [Stylus](http://stylus-lang.com/)). For completeness, this is what the above SASS compiles to: 126 | 127 | ```css 128 | .myapp-Header { 129 | background: black; 130 | color: white; 131 | } 132 | 133 | .myapp-Header-link { 134 | color: blue; 135 | } 136 | 137 | .myapp-Header-signup { 138 | border: 1px solid gray; 139 | } 140 | ``` 141 | 142 | All the usual patterns play well with this, e.g. having different styles for different component states (think [Modifier in BEM terms](http://getbem.com/naming/)): 143 | 144 | ```scss 145 | .myapp-Header { 146 | 147 | &-signup { 148 | display: block; 149 | } 150 | 151 | &-isScrolledDown &-signup { 152 | display: none; 153 | } 154 | } 155 | ``` 156 | 157 | Which compiles to: 158 | 159 | ```css 160 | .myapp-Header-signup { 161 | display: block; 162 | } 163 | 164 | .myapp-Header-isScrolledDown .myapp-Header-signup { 165 | display: none; 166 | } 167 | ``` 168 | 169 | Even media queries work conveniently, as long as your preprocessor supports bubbling (SASS, LESS, PostCSS and Stylus all do): 170 | 171 | ```scss 172 | .myapp-Header { 173 | 174 | &-signup { 175 | display: block; 176 | 177 | @media (max-width: 500px) { 178 | display: none; 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | Which becomes: 185 | 186 | ```css 187 | .myapp-Header-signup { 188 | display: block; 189 | } 190 | 191 | @media (max-width: 500px) { 192 | .myapp-Header-signup { 193 | display: none; 194 | } 195 | } 196 | ``` 197 | 198 | The above pattern makes it very convenient to use long, unique class names without having to keep typing them over and over again. Convenience is mandatory, because without convenience, we will cut corners. 199 | 200 | ### Quick aside on the JS side of things 201 | 202 | This document is about styling conventions, but the styles don't exist in a vacuum: our JS side will need to produce the same namespaced class names, and convenience is mandatory there as well. 203 | 204 | As a shameless plug, I have created a very simple, 0-dependency JS library for exactly this, called [`css-ns`](https://github.com/jareware/css-ns). When combined at the framework level ([with e.g. React](https://github.com/jareware/css-ns#use-with-react)), it allows you to **enforce** a specific namespace within a specific file: 205 | 206 | ```js 207 | // Create a namespace-bound local copy of React: 208 | var { React } = require('./config/css-ns')('Header'); 209 | 210 | // Create some elements: 211 |
212 |
...
213 |
...
214 |
215 | ``` 216 | 217 | Will render into the DOM as: 218 | 219 | ```html 220 |
221 |
...
222 | 223 |
224 | ``` 225 | 226 | This is very convenient, and above all makes the JS side *local by default*. 227 | 228 | But again, I digress. Back to the CSS side of things. 229 | 230 | ### 6. Prevent leaking styles inside the component 231 | 232 | Remember when I said prefixing each class name with the component namespace was a "very effective" way of sandboxing styles? Remember when I said there were "caveats"? 233 | 234 | Consider the following styles: 235 | 236 | ```scss 237 | .myapp-Header { 238 | a { 239 | color: blue; 240 | } 241 | } 242 | ``` 243 | 244 | And the following component hierarchy: 245 | 246 | 247 | ``` 248 | +-------------------------+ 249 | | Header | 250 | | | 251 | | [home] [blog] [kittens] | <-- these are elements 252 | +-------------------------+ 253 | ``` 254 | 255 | We're cool, right? Only the `` elements inside `Header` get [blued](https://www.youtube.com/watch?v=axHe_BVY_9c), because the rule we generate is: 256 | 257 | ```css 258 | .myapp-Header a { color: blue; } 259 | ``` 260 | 261 | But consider the layout is later changed to: 262 | 263 | ``` 264 | +-----------------------------------------+ 265 | | Header +-----------+ | 266 | | | LoginForm | | 267 | | | | | 268 | | [home] [blog] [kittens] | [info] | | <-- these are elements 269 | | +-----------+ | 270 | +-----------------------------------------+ 271 | ``` 272 | 273 | The selector `.myapp-Header a` **also matches** the `` element inside `LoginForm`, and we've blown our style isolation. As it turns out, wrapping all styles in a namespace block is an effective way for isolating a component from its neighbors, **but not always from its children**. 274 | 275 | This can be fixed in two ways: 276 | 277 | 1. Never target element names in stylesheets. If every `` element in `Header` is `` instead, we'll never have to deal with this issue. Then again, sometimes you have the perfectly semantic markup set up, with the `
`s and `