Let's talk about our CSS.
Michelle Bu
★TXJS
Thanks Alex!
Hey everyone. I'm Michelle. Let's talk about CSS today.
I work for a company in sunny San Francisco called Stripe. This is what it looks like outside today.
Stripe is, most simply, an API that helps you accept other people's money.
The project I'm currently working on is a rewrite of our dashboard, which helps our users keep track of how much money they've made and when they'll get paid.
!important
So, why do we have to talk about CSS, you ask? This is a Javascript conference!
For a lot of us, CSS is the thing we write once and forget about, and the only tool we need to know in our day-to-day is this one: [!important].
But no matter how hard you try to forget about those lines of CSS you just committed, it's still there. It'll always be there. It'll probably outlive your other code, and we'll see how this phenomenon happens later.
CSS such a large part of our codebase, but we don't treat it like it is-
Prior to our rewrite project at Stripe, the CSS in our dashboard made up 1/4 of our codebase! That's a quarter of our codebase that people would try to forget about after they finish styling the feature they're working to add. It's understandable that they would do this--developers don't like thinking about CSS. In rewriting our dashboard, we're trying to make it so they don't have to.
has a modal.js, modal.template, and a corresponding modal.css
When we first built our current dashboard we created a handful of reusable views. These then had the honor of having their very own reusable CSS for each template.
Everything else was essentially custom per template. Things that weren't obviously Javascript components did not get considered for any sort of CSS reusability.
This modal component, for example, was first build with forms in mind. So we have a lot of cool form functionality built it, like "show more fields" and labels for optional fields and generally-nice-looking fieldsets.
has a few lines in create_new_account_view.css
However, as we added more modals to the dashboard, we realized we needed more variants of modals.
Instead of adding to the generic modal template and CSS, because, to be honest, it was scary to add styles to a generic component without any style guidelines or regression testing, we'd style them per view.
This particular modal explanation was added 2 years after the modal component was created, and the explanation is styled just for this view with a few lines in create_new_account_view.
.some_specific_view {
.some_specific_section {
background-color: pink;
...
}
...
}
It was easy to style each individual view separately at first, especially since Sass allowed us to nest our selectors. So we could easily go crazy and style away without worrying about affecting other templates.
Except sometimes.
What ended up happening over time was just a build up of modal styles that we just put in their parent views' CSS.
Some developers were conscientious enough to put the CSS for the very specific modal they were styling into a `modals` folder, or leave TODOs saying they'll generalize the CSS one day.
But it didn't really matter if we were conscientious or not. The fact of the matter was that I would never move that CSS back. I wouldn't know how to and it all felt very fragile. What didn't matter at the time I wrote my CSS, though, is that having it in this parent view's CSS is just as fragile. The parent view i'm in might have multiple modals, and now I might've just accidentally introduced broken styles to the other modal. There's no easy way to tell what side effects my CSS might have.
5 minutes in the life of a developer who needs to add some CSS to the existing dashboard.
Now, let me take you into "five minutes in the life of a developer at Stripe who needs to add some CSS"
"I need to add an explanation to this field."
"Let me look for a similar thingy."
My first attempt involves trying to apply the styles from this explanation to my own. But it doesn't work.
"Cool, now inspect element."
So, let's try looking at the CSS.
Now I take the first rule, try to grep for it, because my sourcemaps are broken and I promised that this would only take 5 minutes... and I find nothing.
"git grep 'div:last-child p'" - nothing
And of course I'm going to find nothing, because the styles I'm looking for are heavily nested within the parent view's CSS.
"Alright I'll just take the last one and make it look as close as possible."
Anyways, at this point I just want to stop worrying about CSS and finish everything else, so I take the last set of styles, find the color, and make my own element look as close as possible.
"Donezo!"
And we're done!
Another 8 lines added to our CSS. This is no big deal, until you do for 3 years, between more than fifty different engineers.
So performing this horrible process myself a couple of times and conducting extensive research, I present to you the four stages of CSS degradation--or, how I believe the CSS in our dashboard evolved with our growing engineering team and our changing engineering needs:
1. Determination
(MVP)
Stage one is "Determination." This is where we're like 10 people total in the company. We're starry eyed and eager to ship a user dashboard.
And we're smart, too, so we'll pull in the best tools, like Sass, and clearly define how we want to organize our views and CSS-in a way that makes styling them really easy and development time minimal. All 2 or 3 engineers who work on the dashboard know where everything is, anyways.
So we ship and thing and then celebrate. Whoo, mvp!
1 year and 10 engineers later...
2. Development
(remember: poop is append-only.)
Stage two is "Development." This is when we start building the non-MVP features and enhance the existing features. We add a bunch of new views, and everything's still pretty organized, to be honest, and super easy to write! Sass makes everything so easy! But of course there are inevitably bugs.
And inevitably those bugs will leave some poopy code around, because you're just fixing a bug and it's pretty urgent so you left a few !importants and TODOs in the code but it's okay you'll fix it later.
One thing to keep in mind at this early stage, as Alex has once said to me, "CSS is an append-only language"
1 year and 20 engineers later...
Wow, you're at 30 engineers now! Development's picking up pace. And this is where you enter the third stage of code degradation...
3. Denial
Well, the dashboard still works. Most of the time. I can't see or hear this poop in this CSS that I only touch once or twice a week. And you know what, this isn't even my team's code to maintain.
pooppooppoop
6 months and 40 engineers later...
You're growing exponentially, and so is the poopy CSS. And here is where you enter the fourth stage, despair.
4. Despair
Every time I fix a bug something breaks. Our CSS is growing at a rate of 3 poops per week and there's nothing I can do about it because everything is hopeless.
So let's break down how our existing dashboard's CSS managed to spiral downhill. It's not that in stage 1 we made bad decisions. I think those decisions were 100% the right decisions, and were perfect for our size and expertise and timeline at that time. The way our CSS was handled optimized for fast development and simplicity.
But as the organization grew, our CSS needs started to change. Not every engineer that joined was good at or cared about CSS, or had the context to know exactly how every view in our codebase worked or was used.
Regardless, I believe that if we can start over and talk more about how we write CSS, we could avoid some of these problems. You wouldn't write Javascript without tests or a linter, right? Or generally tolerate poop in other parts of your codebase, so why allow it in CSS?
So, what were the problems?
No tests
"It's almost impossible to test CSS." *
* sorta
Well technically our tests were users writing in about something looking funny.
We'll go into more about this later.
No conventions
.potato { span { ... } }
div.potato > span
div.potato span
.potato-inner
.potato_inner
#potato_inner
Well, technically there were conventions. That each template can have an CSS file. But more specific than that, the actual way the CSS was written had no convention. We'd see everything from nested selectors to very vague selectors to more specifically-named classes of various forms to IDs, even.
No CSS reuse
transfers_list_view.css
Yup
sublist_subscriptions_list_item.css
Nope
And finally, what I think was our most important issue: no css reuse.
Again, technically there was CSS reuse (just not any documented, structured, explicit maintainable kind), in the form of reusing the CSS that styled our "reusable" backbone views. But it was completely undocumented, and never worked in the way you wanted it to, because it wasn't bulit for a variety of contexts. and just looking at a particular view's CSS won't tell you whether or not it's being reused or if it's reusable. There just was no concept of pulling out somethign you think might be reused later, because our Very Organized Folder structure and Very Nested SCSS didn't support it.
Why does it matter?
This causes:
- Too much cruft built up to even tackle the non-MVP problems: mobile readiness, accessibility.
Cycle of more and more poop buildup.
- Progressively more sluggish to build new things.
- Unhappiness when working :(.
and disdain/shame for the codebase, which is unhealthy for the team
The first time around, no one will debate that we made the right decisinos at the time, given the tools available. It's just that a lot of our tools and decisions didn't optimize for future happiness.
So let's start over. This time, let's add CSS to the conversation. Since we built the first iteration of our dashboard three years ago, a lot has changed, and a lot more tools are now available to help us solve our problems, and let us think of CSS as a first class citizen like the other tools that we use.
No CSS reuse
Boot
Let's start by solving the last problem we talked about, no CSS reuse, because solving this one will help with our other problems as well.
We're tackling this problem by building our own component library, Bootstripe.
We've split up our new dashboard designs into composable components, everything from a responsive grid like this property grid,
... that you can compose with other components.
all the way down to the small details, like our "Spinner With Label" component and our
Big Number component, that's just a number that's screenreader friendly, copypastable, supports multiple types of currencies, and most importantly, is pretty big.
Documentation
Documentation is automatically generated from comments in the CSS, and is right up in your face when you're building the component, so you're at least guilted into updating it.
CLICK
Investing the time to build these CSS components may seem daunting at first, but once bootstripe was finished, it was extremely easy to fill a page of our dashboard with these components and worry about the last 10% of the application details.
Investing the time to build these CSS components may seem daunting at first, but once bootstripe was finished, it was extremely easy to fill a page of our dashboard with these components and worry about the last 10% of the application details.
Investing the time to build these CSS components may seem daunting at first, but once bootstripe was finished, it was extremely easy to fill a page of our dashboard with these components and worry about the last 10% of the application details.
Investing the time to build these CSS components may seem daunting at first, but once bootstripe was finished, it was extremely easy to fill a page of our dashboard with these components and only have to worry about the last bit of application logic.
We had thought about the context we would use these components in.
An additional side effect that we didn't quite expect or even try to optimize for was that because we had documentation, other engineers working on internal projects began to use bootstripe..
developers who only knew basic CSS and didn't want to bother with pixel pushing at all began using bootstripe for internal projects, and loved that their projects could, with minimal work, look nice and "Stripe-y" without writing CSS or consulting a designer.
No conventions
.namespace-
ComponentName
(-descendant)
(--descriptor)
.bs-
BigNumber
-delimiter
--comma
Next, let's tackle the previous lack of conventions. Here's what we've settled on in terms of class naming.
This might look familiar to some of you--we borrowed heavily from SUIT CSS naming conventions in coming up with our own.
rework
Before we get too much into that, I'll go over the tools we've adopted for our new dashboard. Sass was amazing for CSS experts and makes their lives much easier, but when most of the folks who work on your CSS only know how to append to the file in semi-arbitrary ways that somehow magically work, it's time to try to find a new tool that will give them less rope. Rework is what we ended up choosing.
Rework is a plugin framework for CSS preprocessing. In its default form it takes the CSS you give it, parses it into an abstract syntax tree, the compiles it into the exact same thing on the other end
rework
Now, you don't need to know exactly what that means, but the gist of it is that during this middle phase, when our input CSS has been parsed, we can write small plugins that will manipulate the abstract syntax tree before it's compiled back into CSS.
rework
This is super cool because rather than pulling in a magical, monolithic CSS framework for CSS non-experts to use, we are now able to pick and choose specific plugins with limited scope so we can dictate exactly how we want our CSS to be written.
Tools make conventions easier.
rework-stripe-conformance
rework-stripe-conformance is forked from rework-suit-conformance, and enforces that the CSS you write follows our defined rules. Not only does the checker yell at you if your CSS doesn't conform, it won't even build it.
As an example, I added .big-number to the big number component, and the build fails with info about where the error is. Our conformance checks also ensure that your selectors are not too generic--they require a class name of the style I described earlier, and yells at you if you try to style tags directly.
Tools make conventions easier.
rework-namespace
.namespace-
ComponentName
-descendant
--descriptor
Let's start simple and just pull in rework-namespace to let us omit that pesky namespace when writing components.
Tools make developers happier.
rework-inherit
Checkbox
Radio button
We also pulled in rework-inherit, a plugin that lets us use very simple, very limited inheritance.
One common way to style checkboxes and radio buttons is to hide the native input element, then style an empty label element to look like a checkbox or radio button. Except one's squarish and the other is circlish. And then there's a second label element that would be the actual label of the checkbox or radio button.
On the left is only a part of the checkbox component, which has a few descendants--a hidden input element, the label that we style to look like a checkbox, and the actual label of the checkbox. On the right is us reusing the checkbox code for the coreesponding three radio button elements. This sort of simple inheritance makes our lives a lot easier, while not being too overloaded and allowing more complex relationships.
Tools make developers happier.
We bundled these rework plugins and more into a custom CSS preprocessor, which we call "KFCSS". This is not yet open sourced, but we're working on it!.
Tools make developers happier.
kfcss
Here's a brief list of the features we've pulled into KFCSS. this includes everything from rework-inherits and rework-namespace ,which you saw in the slides before, to small helpers like rework-move-media, which jus tmoves media queries to the end of the file, and rework-vars, which gives us CSS level 4-compatible variables.
There's nothing too outrageous here, no nesting, no fancy functions beyond a simple "calc".
Again, in general we want to
Limit the scope of features that we're able to use. This way the CSS we write is actually very close to the CSS that is actually applied
Abstraction!
<figure aria-labelledby="us-delim-number-1" class="bs-BigNumber">
<span class="bs-BigNumber-denomination">€</span>5
<span class="bs-BigNumber-delimiter--period"></span>678
<span class="bs-BigNumber-decimal">
<span class="bs-BigNumber-decimal-point">,</span>99
</span>
<figcaption id="us-delim-number-1">€5.678,99</figcaption>
</figure>
{{bs-big-number number=5678.99 country="DE"}}
You're probably making fun of how ugly our class names are. It does, after all, get quite verbose, especially when you're starting to worry about accessibility and user experience and updating the markup of your components from time to time--one thing to keep in mind is that markup is VERY closely tied to your CSS. When you change one, you also need to change the other, so it should also be properly abstracted.
there was a bit of controversy when we first introduced these components, around both the long classnames and the verboseness o fhte markup, but the reality is you hopefully will only be writing HTML by hand ONCE. (In fact, if you did write them by hand in more than one place, whenever your components change, you'll need to change them in every single place.) you likely want to abstract them away with templates. In our case we're using Handlebars, but all templating languages work fine.
but when focussed on writing CSS components and markup that makes sense (instead of just being the simplest possible thing), these classes make it a lot easier to think about how to structure your CSS.
This is not groundbreaking, but abstracting away components make deleting code easier and upgrading markup for css. no more direct coupling of css to your templates.
Abstraction!
If you're using a framework like Ember or React, you can take it a step further and build Ember or react component librararies for your CSS component library
We've build ember-bootstripe and react-bootstripe.
No tests
rework-pseudo-classes
grunt-huxley
The truth is, testing CSS IS pretty hard.
One of the reasons we're ABLE to test now is because we've built a CSS component library. If you've ever had to visually diff a whole page before, you'll know that it's often not very effective--things are always changing, and you don't want to always play Find the super-subtle Difference every time you make a CSS change.
Additionally, the hardest part about testing CSS are the various states your elements can be in. With our small components, we use rework-pseudo-classes to mock these states, so we can declaratively write tests.
We then use a tool called Huxley to take screenshots of our tests and diff them.
In this case, a 2px margin change is very easily and obviously picked up by our visual diffs.
A bonus you get from checking these screenshots in is that you're able to see the diffs on Github when you commit. The general rule for us is that you shouldn't be merging in these commits unless a designer signs off on your changes. These screenshots make it relaly easy to visualize subtle css changes. Our favorite is the onion skin diff mode on Github :).
So while the benefits seem pretty clear, the upfront time investment is still there--why would you take the time to write your own CSS component library?
For us, it made sense because we have our own design language and a fast-growing team. It might not make sense for you, but CSS is definitely something you should still talk and think about when starting new projects.
After building out Bootstripe, CSS is much less a part of our everyday lives than it used to be :).
Developers are able to focus a lot more on the 10% polish and building, and leave the CSS to the CSS experts.
.modal-dialog-view div.modal div.modal-content-form div:last-child p
.bs-Fieldset-explanation
Hopefully a few years from now, instead of trying to figure out what this means when styling an explanation to a form field, we'll know exactly what class to add and it'll work as expected, because it's documented, tested, and we know it follows the conventions that we've built tooling around.
Thanks! :)
@michelle on Github
@hazelcough on Twitter
Resources:
Thanks for coming out today! Most of the team working on our dashboard rewrite project are actually here today, so come by and say hi afterwards! I'll post these slides online in a bit.
My hope is that if we talk about CSS more at the start of a project, we're able to think about CSS less by the end.
Thank you!