DEV Community

Aaron Choo
Aaron Choo

Posted on

Applying OOP concepts to CSS

Series introduction

Recently, Tailwind has seen a large explosion in growth: going from 390,000 npm downloads in Oct 2019, up to 1.8 million downloads just last month. This unprecedented popularity comes with it the Java phenomenon: deeply polarising views within the community where there are those who love it, and those who simply hate it (not to forget those that can't decide as well).

However, this series is not going to be about whether you should or should not be using Tailwind. Rather, this first article will attempt to be an introduction to some OOP concepts, and how they can be applied to writing cleaner CSS. The next article will then dive deeper into the SOLID principles, and how Tailwind relates to all of these. Then, perhaps we can even rationalise why there is such a large divide in the community regarding the use of Tailwind.

Object-oriented programming in CSS

Let's start applying object-oriented programming (OOP) principles to CSS. Firstly, a disclaimer: the idea of object-oriented CSS is not new, and has been around for quite some time. However, there is a lack of articles clearly linking OOP concepts to CSS. If the explanation given here confuses you, you may want to take a peak at the extensive OOCSS wiki. Note, however, that some definitions used here may differ.

Also, there are quite a few concepts related to OOP - and only a couple which can best be applied to CSS. As such, this article will only touch on the following concepts: encapsulation, abstraction, and composition.

Objects in CSS

Firstly, let's define what an "object" is in CSS. In OOP languages like Java, it makes sense to have objects like Car or Elephant which clearly have well defined states and behaviours. What then, represents an object in CSS? For simplicity's sake, let's define an "object" in CSS to be synonymous with selectors.

For example, here is a class selector and an id selector, both with defined styles within them, and which we can refer to as two different objects, .object-1 and #object-2:

.object-1 {
  /* styles here */
  /* ... */
}

#object-2 {
  /* styles here */
  /* ... */
}
Enter fullscreen mode Exit fullscreen mode

Note that this definition is not official, and is nothing more than syntactic sugar made to drive the concept closer to OOP. With that, let's start!

Encapsulation in CSS

Encapsulation is the process of grouping logically related data and operations together in an entity. Basically, things which make sense to be together, ought to be together. Every block of style you write in a CSS selector (or "object" as we have defined) can essentially be taught of as a single encapsulation. Note that there are good ways to encapsulate code and bad ways to encapsulate code.

As an example, let's try building a simple HTML button. This button should have a 2-pixel border that is cyan in colour, white text, and a teal background (yes, gruesome design, I know). In the CSS of the following snippet, we have a #teal-button object, which is an encapsulation of the styles needed to style the button:

<!-- HTML -->
<button id="teal-button">Press Me!</button>

<!-- CSS -->
<style>
#teal-button {
  border: solid 2px cyan;
  color: white;
  background: teal;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Now, what happens if we want to have a similar button, except with a red background this time? Well, if we do not want to refactor our previous code, here's an easy way:

<button id="teal-button">Press Me!</button>
<button id="red-button">Press Me!</button>

<style>
#teal-button {
  border: solid 2px cyan;
  color: white;
  background: teal;
}

#red-button {
  border: solid 2px cyan;
  color: white;
  background: red;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Notice that we have redundantly duplicated portions of the CSS. Is there a need to have the same styles for border and color in two separate places? This is a classic example of the violation of the aptly named Don't Repeat Yourself (DRY) principle. In OOP, we would have said that the #teal-button object and the #red-button object have similar states which could be better aggregated or encapsulated into a common, shared object:

<button id="teal-button" class="base-btn teal-bg">Press Me!</button>
<button id="red-button" class="base-btn red-bg">Press Me!</button>

<style>
/* contains styles common to both buttons */
.base-btn {
  border: solid 2px cyan;
  color: white;
}

.teal-bg {
  background: teal;
}

.red-bg {
  background: red;
}
</style>
Enter fullscreen mode Exit fullscreen mode

As your app grows and as you add more buttons of different background colours, using the first approach means that your CSS will grow extremely fast. Refactoring will also be an absolute nightmare. For example, in the second approach, a single line of CSS has to be modified to make all buttons change their borders to green. How many lines of CSS do you need to modify in the first approach for the same effect?

It is easy to reason to yourself how the second approach promotes code reusability, while reducing unnecessary code duplication. Writing less CSS comes with it two clear advantages:

  • Smaller bundle size for your built website
  • Much better developer experience (no more waddling over pages of id selectors, or changing multiple pages of CSS just to have a consistent design)

Abstractions in CSS

Abstraction is the process of hiding the internal working of a process from its end user. Do not confuse this with encapsulation, which is the act of grouping things together in an entity. Abstractions rely on encapsulations: we group things together in an entity via encapsulation, and then expose that entity as an abstraction for the end user to use. This way, there is no need for the user to know how the entity is implemented, only what it does.

In CSS, the end user is actually yourself and your fellow developers. Using the same example above, we have actually practised sound abstractions by defining the teal-bg and red-bg classes. There is no need to look at the actual implementation of the CSS classes to know that the teal-bg class is obviously applying the teal colour to the background.

Similar to encapsulation, there are good and bad ways to abstract things. In the previous example, the base-btn class does not immediately give us a visual understanding of how the button is implemented. Does this mean that the base-btn class is bad? Well... It depends, and we shall leave this to the next article. What we do know is that the act of using semantic names for classes is one of the ways abstractions can be effectively used in CSS.

Ok, you might be asking, but what is the point? Let's have another example, where we want to make a button, and a paragraph, both with blue texts. If you have been paying attention to the previous section, we now know that we should avoid repeating the same code via encapsulation. As such, we can come up with this:

<!-- HTML -->
<button class="blue-text-button">Press Me!</button>
<p class="blue-text-button">Here is some text...</p>

<!-- CSS -->
<style>
.blue-text-button {
  color: blue;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Yes, this fits the design specifications perfectly fine, and we have also managed to not repeat ourselves. But the issue with this is that the HTML/CSS has lost their semantic meaning. Without looking at how the CSS is implemented, do you know what exactly the blue-text-button class does? Should the button word be part of blue-text-button if it does not even style a single part of the button? More importantly, why does the p tag, which clearly isn't a button, depend on the CSS class whose name has button in it?

In order to let your fellow developer (or yourself in the future) understand immediately what you are trying to achieve, a better approach will be to rename the blue-text-button class to blue-text. Now, with sound abstractions, it is immediately obvious what this snippet is trying to achieve, without even looking at the implementation of the CSS:

<button class="blue-text">Press Me!</button>
<p class="blue-text">Here is some text...</p>
Enter fullscreen mode Exit fullscreen mode

Of course, one could argue that the above CSS implementation is trivial, and does not detract one from immediately understanding what blue-text-button does by looking at the CSS implementation. What about this case where your website has a custom colour palette:

<button class="custom-colour-1">Press Me!</button>
<p class="custom-colour-2">Here is some text...</p>

<style>
.custom-colour-text-1 {
  color: #51c6e0;
}

.custom-colour-text-2 {
  color: #5be051;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Even looking at the CSS implementation wouldn't save you (unless of course, you memorised the hex colours...). Assuming the colours are part of your company/brand, and which probably will not change for quite some time, a better approach would be to name the classes like so:

.brand-blue-text {
  color: #51c6e0;
}

.brand-green-text {
  color: #5be051;
}
Enter fullscreen mode Exit fullscreen mode

Now, you can safely use the class brand-blue-text or brand-green-text anywhere in your HTML, where you and your fellow developers will immediately know that what you are trying to achieve.

Compositions in CSS

Composition is the act of modelling a has-a relationship between objects. For example, a car has an engine, and a human being has a heart. Again, if you are sharp enough, the use of composition have already been demonstrated in very our first example. Instead of putting all the styles into #teal-button, we have instead defined two different classes, .base-btn and .teal-bg, from which we then compose together to make the original style of #teal-button.

The whole idea of composition is to reuse existing code, reduce unnecessary code duplication, and to design cleaner CSS (basically the whole point of this series if you haven't got the memo yet). Let's reuse our first example again, except with one more green coloured button this time:

<button id="teal-button" class="base-btn teal-bg">Press Me!</button>
<button id="red-button" class="base-btn red-bg">Press Me!</button>
<button id="green-button" class="base-btn green-bg">Press Me!</button>

<style>
.base-btn {
  border: solid 2px cyan;
  color: white;
}

.teal-bg {
  background: teal;
}

.red-bg {
  background: red;
}

.green-bg {
  background: green;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Now, say for instance we want to add a 8px padding around the teal and red buttons only (ie. the green button should stay as is), what would be the best way to do this?

As our first try, we could define this padding within .teal-bg and red-bg:

.teal-bg {
  background: teal;
  padding: 8px;
}

.red-bg {
  background: red;
  padding: 8px;
}
Enter fullscreen mode Exit fullscreen mode

However, this violates what we've learnt in abstractions: it's utterly confusing for the class teal-bg to also apply padding when the name of the class clearly has no mention of padding in it. To remedy this, we could rename the classes, which would then require us to refactor all the instances in which the original teal-bg/red-bg has been referenced - not ideal.

For our second try, we can instead put the padding style in base-btn, which would require an !important in .green-bg to override the padding (remember that the green button also takes .base-btn as a class):

.base-btn {
  border: solid 2px cyan;
  color: white;
  padding: 8px;
}

.teal-bg {
  background: teal;
}

.red-bg {
  background: red;
}

.green-bg {
  background: green;
  padding: 0px !important;
}
Enter fullscreen mode Exit fullscreen mode

Now, however, we've made our CSS really fragile and dangerous. For a component with a defined padding, applying green-bg to it will lead to that component losing its defined padding - not ideal.

The better approach is to define a class just for the padding, which we can then compose with the teal and red buttons like so:

<button id="teal-button" class="base-btn teal-bg padding-8px">Press Me!</button>
<button id="red-button" class="base-btn red-bg padding-8px">Press Me!</button>
<button id="green-button" class="base-btn green-bg">Press Me!</button>

<style>
.base-btn {
  border: solid 2px cyan;
  color: white;
}

.teal-bg {
  background: teal;
}

.red-bg {
  background: red;
}

.green-bg {
  background: green;
}

.padding-8px {
  padding: 8px;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Now, this looks much better! We have encapsulated the common padding style into a single class, and abstracted it such that the class name padding-8px makes sense. By not modifying the other existing classes, we have further reduced the risk of breaking anything which relies on them. Notice also, that by reading the HTML only (without reading the CSS), we can still visualise roughly how the end product looks like.

Conclusion

That's it, now you're on you way to writing cleaner CSS! We have only just scratched the surface here regarding OOP and object-oriented CSS. In the next article, we shall dive more deeply into exactly why some of the negative examples shown here are bad, and actually start looking at Tailwind and the SOLID principles.

Top comments (0)