CSS: Tell, don't describe

20 December 2017

Writing an article which illustrates my take on Atomic CSS helped me put words on how I feel CSS should be written and maintained.

My preferred method of writing styles is neither purely functional or component-based. The method I would like to propose is closer to how back-end code is handled.

There is an Object Oriented programming approach called Tell don’t ask. At its core it can be surmised as follows:

Rather than asking an object for data and acting on that data, we should instead tell an object what to do.

It is a simple principle that has big implications while writing object classes. Using this method, you tend to generate objects that encapsulate their internal logic while exposing their intent as public methods. Objects with fewer getters may be harder to manipulate in unintended ways.

Personally, this approach helps me apply the Single responsibility principle by limiting public access to object data. I, therefore, tend to make multiple objects that each does one different thing instead of overusing getXYZ() methods from a god class that does multiple different manipulations.

Applying it to CSS

I find the Tell don’t ask method to be an elegant way of writing CSS as well. The implementation of that method results in a variation of Trait sharing applied to DOM elements.

I propose to call this behavior-centric methodology “Tell, don’t describe”.

I would define it as :

Rather than describing how an object should look, we should instead tell an object what it should do.

What I am trying to achieve

To clarify the points I am trying to improve with this method, I have taken screenshots from projects that I have come across. These examples are real in the sense that competent people have written it in the context of employment.

My goal is not to say there is a problem with the developers themselves, but rather to suggest that maybe the approach they have used to write their styles allowed for smells to occur too easily.


Prevent deep component styles

Deep in the rabbit hole Deep in the rabbit hole

Because you are coupling the HTML hierarchy inside the style rules, you may have to deal with selectors that suddenly disconnect, or which lose specificity as the HTML source changes.

We want to be able to modify the views without breaking the styles as much as we can.

We also want to discourage adding new edge case in the existing component styles and make it harder for a developer to decide to create a copy of the component styles that would only manage the extra HTML tags.

Optimize file sizes

Even Webpack thinks this is huge Even Webpack thinks this is huge

Unless you spend a very large amount of your time actively refactoring and regrouping code to optimize reuse in an ongoing project, and unless you maintain a thorough grasp of the full code base in your mind at all times, you will end up with many duplicate style concepts in your files.

Sometimes, you may not be able to completely extract common assignments between styles because their varying children DOM elements may depend on common top-level assignments.

We want to make style assignments cascade at an HTML element level to decouple the styles from the children HTML tags as much as we can.

We would rather leverage natural style inheritance through the class attribute rather than import style mixins in order for us to maintain few complex selector chains and juggle fewer overrides at during style declarations.

Obvious intentions

I don't know how you look I don’t know how you look

It is hard to modify existing template code if you do not have good chunks of the project’s CSS in mind when looking at its HTML. It is not a realistic expectation to think developers on maintenance duty will completely ingest a project’s personalized differences at a glance before working on their issue.

We want to be able to understand the visual intentions of template elements by looking at the least possible number of files possible.

What happens when you inject behavior

One can solve the issues raised previously by attaching behavior to HTML elements instead of describing them. CSS then becomes a collection of simpler visual behaviors that you can grant, deny or chain as CSS class names. It is irrelevant whether you use pre or post processors to do so.

From font sizes and margins to animations and colors palettes, everything can be described as a reusable behavior. The following HTML, even if you do not know how the CSS is built, clearly denotes its visual intentions:

<div class="about-us-cta horizontal-children alternate-font light-margin show-hint-on-hover">
<div class="hidden hint">I will display when you hover my parent</div>
<div>Lorem ipsum</div>
<div class="centered-horizontally takes-half-width">Lorem ipsum</div>

Of course – if it is still required at all after applying common behavioral styles – you may then apply edge cases on a custom selector (.about-us-cta in this example). The custom rules will be much simpler and less lengthy because there is nothing really left to modify that hasn’t been described by the behavioral assignation. Such differences may even be bounced back to the design team as possible inconsistencies in their work.

In most cases, you can reuse common behaviors across components that have no direct relations to each other by assigning the common behavior classes.

In order words, though the .houses and .cars classes may both contain .windows (or to give a more CSS-oriented example, may have a similar border treatment), it may still not make sense for the two concepts to be grouped under a common style named .all-things-with-windows.

Grouping the two slightly different concepts under the same parent may influence developers in adding .wheels-related assignments (maybe different margins) within that common group. These would have no visual impact for .houses but their use-case would still have to be taken into account when modifying the styles.

It may be more useful to create a .has-windows behavior that minds its own business and can be graphed to any other context, like .has-wheels. You would end up with an HTML element defined as <div class="has-windows has-wheels">Lorem ipsum</div> and the CSS declaration of these two classes would not be intertwined.

That is what I feel is the core strength of behavioral CSS. By defining clear and unchanging behaviors you greatly limit selector depth in the style declaration. It allows you to pick and chose behaviors to add to any DOM element.

Because the behavior does not attempt to handle deep elements (or at least tries very hard not to), you should not have to override styles on descendant elements. Since you don’t have to care which styles may or may not have been defined by an ancestor, you can attach behavior to these other elements as well and similarly continue down the DOM tree.

In a chain of behavioral class names, the intention of the element becomes obvious because it is explicitly set in the HTML. By glancing at the source file you get a quick idea of what that block of HTML is doing visually. Adding or removing behaviors can be estimated easily by glancing at the assignment chain.

In this context, it seems reasonable to expect developers to remember a limited array of repeated visual concepts within a project. They could even be able to change the visual behavior of elements only by modifying template files.

This is not functional CSS

You cannot describe this approach as being “Atomic” or “Functional”. The goal here is to code explicit behaviors for a project and not to generate a list of tools that map styles – often one-for-one – to be peppered in the HTML file.

In this behavioral minded method of writing CSS, it is more important to encapsulate the meaning of what it does rather than to encapsulate whether it is aligned using flex or floats. It is important to see that the ways styles are defined within a behavior class do not matter as long as the exposed visual results remain the same.

In that way, it is more important to tell elements what to do rather than to describe them.