Precursor: Common CSS Problems
If you are already familiar with the many challenges of writing clean CSS, feel free to jump forward to my thoughts on the individual frameworks. To offer a bit of context to my thoughts, I’d like to go through why you should even care about using these new frameworks instead of just plain ol’ CSS.
Although the C of CSS stands for Cascading, it doesn’t mean we should abuse this power. More often than not, using nested selectors leads to unintended complexity with too many layers of specificity. The more layers of specificity there is, the harder it gets to change the styles of that element without resorting to !important. Relying on inheritance is also problematic as it ties our styles to the structure of the HTML. If the HTML changes, we need to make sure our CSS is adjusted accordingly. If we had used a unique class to target that element, then changing the HTML around would not affect the CSS. Of course, there are times when using some inheritance in our selectors is unavoidable (for example, when overriding a third party library’s styles), but generally we should try to keep specificity as low as we can.
Isn’t the point of using classes so you can reuse them? Yes, but reusing classes often results in unintended side effects. It’s easy to forget where you have reused your classes, and changing the style to fit one use-case can inadvertently break a different use-case elsewhere in your app. A great way to have reusability in a React app, would be to instead have components that can be reused instead of classes. This way, that one class is always tied to the same component, making it easy to see what changing the class will affect. Again, this isn’t to say you should never reuse classes. Having simple utility classes that are unlikely to change are great ways to keep consistent font/padding/margin/etc. (But more on utility classes in the Tailwind section 😉 )
If CSS is not broken up into files/sections, it becomes hard to find and edit the CSS you are looking for. In traditional websites, there is generally only one
styles.css , making organization difficult to upkeep. Long stylesheets that no one understands often result in what we call “append only stylesheets”. If developers can’t easily find what they need to update, they will just append new CSS to the bottom of the sheet, instead of maintaining what they already have.
The crux of CSS problems really boils down to one thing: CSS can get unwieldy very quickly if not properly managed. We would like to avoid too-long sheets, deep nested selectors, and unnecessary !important’s. Each of the following CSS methodologies approach solving these problems in their own way.
Style 1: Sass
At the core of it all lies CSS, the bread and butter if you will. This is what every web developer should learn first, and in many cases plain ol’ CSS is all that you’ll need. Setting the foundation for your learning is important, and should be done prior to learning any new “fancier” ideas.
Sass is a preprocessor scripting language that is compiled into CSS. Sass is an improved way of writing CSS, that gives developers powerful tools like variables, mixins, and nesting, basically it makes writing CSS simpler and more efficient.
Sass lets you use powerful tools that you can’t in vanilla CSS, like variables, mathematical operations, mixins, loops, functions, imports and other interesting functionalities that make writing CSS much more powerful. I won’t go too in-depth in these as that is a whole blog post in and of itself. Basically, you can think of Sass as writing CSS, but with more features!
Although this feature isn’t exclusive to Sass, using bundlers like webpack allows you to split your Sass (or CSS, in the case of using CSS modules) into files. This is valuable as it helps organize code, making CSS maintenance easier.
A big benefit of Sass (compared to the other two frameworks in this post) is the freedom to write your own CSS how you want. At TTT we generally stick to the BEM (Block Element Modifier methodology, however writing your own Sass lets you write your CSS however you think is best.
As we just read, the freedom that Sass gives you allows you to write code in whichever way you want, which can be great, but is also dangerous if misused. As the saying goes “With great power comes great responsibility.”
Many developers, such as juniors or backend devs, don’t have a complete understanding of CSS, which can lead to unnecessarily complex CSS that future developers are too afraid to touch in fear of breaking things. Because Sass offers such freedoms, these developers aren’t “reined in” by the framework. This misuse can be avoided by having solid CSS guidelines and code review, however, it is on the developer (or team of developers) to enforce those guidelines.
Picture this: You are working away on your component, creating
headers. You know these will need some styling, so you assign some class names that you think make sense! Now that you’re done your markup, you switch over to your CSS file to start styling and….you’ve forgotten what the class names were and have to tab back to your markup. If this hasn’t happened to you, well then your memory is better than mine.
The fact remains that there is a disconnect between your styles and your markup. Many of you may be thinking, but that’s a good thing, “separation of concerns” right? And yes, as developers we live by this principle, and at first glance, yes we should keep the markup (the content) and the styling separate right? However in practice, styling and markup are not separate concerns at all! How many times have you had to add a
div purely for styling purposes? Markup and styles are so intertwined now, we as frontend developers are constantly tabbing between our HTML and CSS, causing a maybe minor inconvenience, but an inconvenience nonetheless.
There are only two hard things in Computer Science: cache invalidation and naming things.
— Phil Karlton
You may have heard this joke before, but it’s true! Naming things is hard! BEM is a methodology that aims to help us come up with meaningful class names, and it does help. However, you do still have to spend time thinking about meaningful blocks, with appropriate elements and modifiers.
It also gets tedious naming every single class for every single element you need to style, since we don’t want to mix inline styles and stylesheets. It results in a lot of classes being created simply for adding a width: 100% or some margin or padding.
Sometimes we just want to add one or two little styles. Especially when it comes to layout, we don’t exactly need a reusable class that is in use only in the first specific part of your page telling the buttons how they should sit next to each other. It is a pain having to make one and come up with a name for it, which leads us nicely into our next methodology….
Style 2: Utility-First CSS (featuring: Tailwind CSS)
Utility-first CSS is a methodology that came about in an effort to solve all the cons mentioned above with writing traditional CSS. Utility-first CSS involves using many small helper classes to make up a component’s styles, instead of writing class names based on semantic meaning. One thing to keep in mind is that utility-first CSS doesn’t need any library, it is simply a methodology that can be applied to CSS, similar to how BEM works.
I will be focusing on the library Tailwind CSS, since it is probably the most popular approach developers are flocking to for implementing utility-first css.
Quick introduction to Tailwind: Tailwind CSS is a utility-first CSS framework packed with classes like
rotate-90 that can be composed to build any design directly in your markup. What does that actually look like? Here’s an example.
Here, you can find a chat notification implemented with classic CSS:
Here, is that same chat notification implemented with Tailwind classes:
Right off the bat, you’re probably thinking “Ew, talk about hard to read markup.” Even the developers of Tailwind will admit, in terms of first impressions, it’s not great, and that you have to try it to believe it! When I first looked into Tailwind, I was skeptical. I value readable code, and this seems like the opposite so why are so many developers swearing by Tailwind’s approach to utility-first?
Tailwind CSS: Pros
As mentioned in Sass’s cons, context switching between files can hamper a developer’s focus and result in lost time. Tailwind solves this hassle and allows you to write your styles right there inline with markup. What some may consider a small headache, may be a huge time saver for others.
Again, as I have brought up in the Sass cons list, naming is a frustrating thing to navigate. With Tailwind, you are no longer coming up with your own classes for each element, you are instead making up an element’s styling using their provided utility classes. For example, if you just need a flexbox quickly, it’s very easy to throw a
flex justify-center onto a div and you’re all set.
Your CSS Stops Growing
Tailwind CSS provides you with a set of utility classes to use and besides those, you very rarely will need to write your own custom CSS, resulting in smaller CSS output.
If you are worried about including all the extra unused utility classes Tailwind provides, you can use a tool like PurgeCSS that will remove all unused CSS.
Making Changes Feels Safer
Because the styling is done in the markup instead of an external stylesheet, when you need to change styles, it’s easy to see exactly what you are updating. There are no unintended side effects elsewhere in the app.
You Can Still Create Components
You might be thinking, “okay utility sounds nice but what about when I want a button to have the same style throughout the app?”. Good news, it’s called “utility-first” not “utility-only.” You can still create regular classes while using Tailwind! If you need a button component, you can easily extract your Tailwind code to reusable classes
As you start to write utility-first css, you may come to the realization that most “components” don’t end up being reused anyway. It might seem necessary to have a class for your “Notification Bar” component, but will that class actually get reused somewhere? Probably not! You may use your
<NotificationBar /> component multiple places, but if your code is already extracted to its own component, what need will those classes have elsewhere?
Browsers cache static assets, which is good for performance, but if you just need to make some CSS style tweaks to your website, you then need to purge the file from cache. With Tailwind however, since the styling is within the markup now, you don’t have to worry about invalidating your css assets.
Hard to Read Markup
Readability definitely suffers when using utility classes. Especially if you need different class names for different breakpoints or hover events, you can very quickly end up with very long class names.
I couldn’t even screencap this whole line for an image. Just know that this scrolls on about another screen width…
There is an initial learning curve that comes with moving to utility classes. When I started experimenting with Tailwind, I found it frustrating knowing exactly what style properties I wanted to apply, but didn’t know the Tailwind equivalent, and had to keep tabbing to a Tailwind Cheat Sheet (this was a life-saver). Once you are more familiar with the classes, I can see this speeding up drastically, and actually increasing dev speed. In the beginning, however, there will definitely be a bit of a hump to get over.
No more ‘cascading’
I put this in the ‘cons’ section because when I first started learning about Tailwind, I thought it was one. However, the loss of the ‘cascade’ is actually a GOOD thing.
Specificity has got to be the number one source of CSS bugs. If you have ever had to override third party styles then you probably know the pain of jumping over 5 levels of specificity before you:
a) Find the level you need and successfully apply your style
b) Give up and slap an !important on the end. (Assuming there weren’t already
!important‘s within the existing CSS 😱)
Using class names and avoiding child selectors is something one can do while working with regular CSS of course, but it is up to the developer to enforce that rule. With Tailwind, it is already enforced by the framework.
Overall, utility-first CSS using Tailwind seems like a solid pick. I think the biggest draws of Tailwind are the speed at which it allows developers to style elements, as well as limiting the chance for misuse. That isn’t to say Tailwind can’t be misused. Everything can be, but there are less ways to misuse it than regular CSS.
Style 3: CSS-in-JS (featuring: styled-components)
On to the last styling methodology: CSS-in-JS, with a focus on React.js library styled-components. CSS-in-JS has similar ideas to Tailwind regarding
Naming + Context Switching
Same as with utility classes above, styled-components also alleviates these two problems with CSS. Your CSS code lives right in the same file as your markup and you do not have to worry about switching back and forth.
Again, like Tailwind, styles are scoped to a specific component. This results in way easier maintenance since styles can be found side by side with the markup they refer to. The worry of affecting anything else within the app is eliminated.
This also enforces best code practices when it comes to making small, reusable components instead of huge, long components that contain what could be multiple components. By forcing you to think about each element you want to style as a component, it also enforces extracting away reusable React components where possible.
JS Functionality within CSS
Since your CSS code lives within your JS code, you can use any JS functionality you might need to determine styling. For me, this is one of the biggest draws of CSS-in-JS. Traditionally this would need to be done with classes, so if you had 5 different button types, you would need the corresponding classes for each type. With styled-components, you can handle it all in JS which makes your workflow much smoother.
This is especially useful for theming + styles based on user input
In the past it’s been said that performance has historically been an issue with styled-components. From what I’ve seen, however, it seems that in their new v5 release they claim to have made vast improvements to both performance and bundle size, boasting an impressive 26% smaller bundle size, 26% faster updating of dynamic styles and 45% faster server-side rendering. You can find more information regarding the v5 updates in this Medium article.
So, the million dollar question, “Which approach is the best?”
While our Web Team here at TTT is used to writing Sass, and enjoys it, I can say that trying out either Tailwind or styled-components is intriguing. Our Mobile Team currently uses Tailwind with React Native and a couple of our devs swear by it. It would be nice for our Web and Mobile teams to be united on our styling framework of choice, to help cohesion between the two stacks. On the other hand, styled-components is tightly coupled with React and the React way of thinking about components, so it would fit very nicely with our current React stack.
As is typical for programming frameworks, there is no solid answer here, there are a lot of personal opinions involved, and different developers will have more success with different frameworks from others.
Featured image credit: @grakozy