In enumerating the costs of technological progress, Freud followed a depressing line. He agreed with Tams’ observation that our invention was merely an improvement of the means, but not of the ends.

— Neil Bosman, Technology Monopolies

Although development tools have long evolved from Preprocessor to Styled Component and even Functional CSS, in my opinion the new tools do not make style code better, just faster — and may make code break faster. Rather than making the underlying problems that make code difficult to maintain disappear, the tools boom makes it easier to ignore them. This article aims to answer the question: Why is style code hard to get right, and what are its pitfalls?

If the formal structure of the conversation, the routine is mostly in accordance with some important characteristics in turn to explain. But these so-called important characteristics in the field of programming is universal, such as “extensibility”, “reuse”, “maintainability” and so on, according to this way of thinking, talk more than application. So why don’t we look at how style code should be written and organized by solving a specific style problem

Here is a very simple popup component that we will string together the entire content in its style.

Let’s start by implementing it in a crude way. Intuitively, we only need three elements to implement this popup: div is the outermost container, H1 is used to wrap the “Success” copy, and button is used to implement the button

<div class="popup">
  <div>Success</div>
  <button>OK</button>
</div>
Copy the code

I won’t write out the complete style, but just outline some of the key attributes

.popup {
  display: flex;
  justify-content: space-around;

  padding: 20px;
  width: 200px;
  height: 200px;

  div {
    margin: 10px;
    font-size: 24px;
  }

  button {
    background: orange;
    font-size: 16px;
    margin: 10px; }}Copy the code

The first version implementation is complete. There seems to be nothing wrong so far.

The problem is not implementation but maintenance. Next, I’ll look at some common practical requirements changes to see how the code above is problematic.

Dependencies on DOM elements

Suppose you now need to add a new element below “Success” to show Success details

Of course we need to add a new div tag. However, the.popup div style in the above style would have the same effect on both divs at the same time, which is not desirable because the styles of the two elements are obviously different. OK, if you insist on using labels as selectors, you can use the pseudo-class selector nth-Child to distinguish styles:

.popup {
  div:nth-child(1) {
    margin: 10px;
    font-size: 24px;
  }

  div:nth-child(2) {
    margin: 5px;
    font-size: 16px;
  }
Copy the code

But if one day you decide it would be more appropriate for “Success” to be wrapped in h1 instead of div, the cost of the change would be:

  • Change div to h1,
  • willdiv:nth-child(1)Change the style to h1,
  • willdiv:nth-child(2)Revert to div style

But if you can give button and div an exact class name from the start, then when you modify the DOM element, you only need to modify the DOM element, not the style file

The example above is a horizontal extension situation, where I add an element at the same level as an element. The same goes for vertical scaling, and you can completely imagine a selector that looks something like this:

.popup div > div > h1 > span{}.popup {
  div {
    div {
      span{}}}}Copy the code

In either case, the style depends heavily on the DOM structure. In a series of DOM tag hierarchies, even a single element that goes wrong (either because of a change in the element’s tag type, or because of a new element added to it) can result in widespread style failure. If you want to reuse.popup div > div > h1 > styles, you have to copy the DOM structure to where you want to reuse it.

So at least one conclusion can be drawn here: CSS should not rely too much on HTML structure

The word “excessive” is added because styles cannot exist independently of structure, and dependencies such as.popup.title.icon imply the rough outline of HTML structure.

So we can go ahead and modify the above principle slightly: CSS should have minimal knowledge of HTML. Ideally, a.button style should look like the same stereoscopic clickable button whenever applied to any element.

Parent element dependency

The components we developed in the previous section will often be referenced in multiple places on the page, but there will always be individual cases where you need to modify the components to fit. Suppose there is a need to use popup on their mobile site, but to fit mobile devices, some elements such as length, width, inside and outside margins have to be reduced, how would you do that?

Ninety percent of the solutions I’ve seen are implemented by adding parent dependencies, which determine if the component is under a particular class and modify the style if it is:

body.mobile {
  .popup {
    padding: 10px;
    width: 100px;
    height: 100px; }}Copy the code

But if you need to add a new style to the tablet at this point, I’m guessing you might add a body. Tablet {.popup {}} code. If there are two places on a mobile site that need to use PopUp, your code will probably end up like this:

body.mobile {
  .sidebar {
    .popup
  }
  
  .content {
    .popup}}Copy the code

Such code is still hard to reuse. If a developer sees the popUp opening style on a mobile site and likes it and wants to port it to another site, it’s not enough to just import the PopUp component; he needs to find the actual code that works and copy and paste the style and DOM hierarchy.

Relying too much on the parent component to adjust the style indirectly, when a component already has its own style, is a case by case code behavior that essentially overrides the popup style. Assuming popup has a box-shadow style property, but in some cases box-shadow may be accentuated, and in some cases box-shadow may disappear, its box-shadow root is meaningless because it will never take effect.

Overhead violates the principle of least surprise and brings “surprises” to subsequent maintainers. If the popup design is changed at this point and the shadows need to be reduced, changing the style itself will not work, or will not work everywhere. The maintainer doesn’t know what else doesn’t work and why, and he also needs to look at the code case by case. This undoubtedly increases the cost of modifying the code

Solving this problem is not as simple as solving DOM dependencies and requires a multi-pronged approach.

Separation of style roles

Separation of concerns is always a proven way to improve code maintainability. Looking at existing methodologies for organizational styles, such as SMASS or ITCSS, the appropriate role division of styles is one of their core ideas.

Let’s take a full popup style as an example:


.popup {
  width: 100px;
  height: 30px; 

  background: blue;
  color: white;
  border: 1px solid gary;

  display: flex;
  justify-content: center;
}
Copy the code

In this set of styles, we see that

  • There are width, height that are related to the layout
  • Background, color associated with visual styles
  • Own layout style flex
  • Other styles such as border

Based on these characteristics and common specifications, consider separating styles from the following dimensions:

  • Layout and size: It is normal for a component to have different dimensions under different parent components. Rather than define a size that is overhead and ready to be overridden, leave the layout to dedicated components. On the other hand, the component does not own dimensions, for example it can choose to always fill the container that wraps it with 100% width and height

On the surface, this behavior simply transfers style (size) from one component to another (container), but it fundamentally solves the parent dependency problem we mentioned above. Any other component that wants to use PopUp doesn’t have to worry about how the size of the PopUp component is implemented, it just has to turn itself off.

At a deeper level, it eliminates dependency. You may not have noticed that the Flex layout style configuration follows this pattern: When you want your child elements to be laid out according to certain rules, all you need to do is change the parent element and the Flex layout style properties, without changing the child element style at all.

Another example of an anti-pattern I personally think is text-overflow: The ellipsis attribute alone is not enough to automatically omit text inside the container. The container also needs to be 1) the width must be px pixels and 2) the element must have overflow: Hidden and white-space:nowrap styles. That is, when you want to implement A, you have to rely on the implementation of B and C.

As for whether the layout functional elements are the same element as the parent element or independent element, I prefer the latter. After all, a few markup codes will not add much burden to us, but the clear division of responsibilities can bring us a lot of convenience in the future maintenance

In this context, any layout style you add to popup actually means you’re adding an implicit dependency, because you’re actually implying that its margin looks just right under the parent container.

  • Modifier: The open-closed part of the SOLID principle tells us to close for modifications, but for extensions. The same holds for style code.

Usually we don’t just need a single style button, we might also need an error style button with white text on a red background, and a warning style button with white text on a yellow background. A common solution to this use case is not to create N different button styles, such as primary-button, error-button (so there must be a lot of common button code), butto build on a single button style, This is achieved by “decorating” classes by providing styles. For example, the basic button class name is Button, and if you want it to have a warning style, just use the error class name as well

<div className="button error"></div>
Copy the code

This is essentially a separation of concerns, but from this point of view it is concerned with “change” and “not change”. We move all “variables” into the “modifier” class.

However, there will be many problems in the implementation of this scheme. The first one is the design of the modification class. For example, when I define the modification class such as Error, primary and warning, which style attributes can be overwritten and which cannot, there must be a prior agreement. Otherwise, someone writing an Error style might mindlessly overwrite the style on the original button until it looks good. It depends on the ability to abstract, but bad abstractions are harder to maintain than none.

  • Modularity: With component modularity in the wind, style modularity seems to come naturally. But taking a longer view, modularity is not just about shoving styles into a corner and encapsulating them into centralized management. As you can see from the above example, borrowing the parent element dependency features of the style can easily break this encapsulation.

Components are not the only unit of encapsulation style. In a web site, there may also be global or faceted style attributes such as Base and reset. My ideal modular style should easily accomplish the following:

  • Control the directionality of style influences: for example, global styles can affect components, but components cannot affect the world
  • Isolation and contamination between style modules: Although A component is A child of B, the style of B component does not affect the style of A

The best example of both is the industry-wide font size adaptation solution for responsive development. For example, the following component’s HTML structure

	<div class="ancestor">
	  <div class="parent">
	    parent
	    <div class="child">
	      hello
	    </div>    
	  </div>
</div>
Copy the code

In the style we will set:

  • The ancestor component font changes relative to the HTML of the root element, so use the REM unit
  • The font units of the parent and child need to be changed relative to the base font of the component (that is, its ancestor), so the EM unit is used
.ancestor {
  font-size: 1rem;
}
.parent {
  font-size: 1.5 em;
}
.child {
  font-size: 2em;
}
Copy the code

This way, when we need to adjust the font size for the device, we only need to adjust the HTML font size for the root element, and the rest of the page will adjust itself. If we just want to resize the local style, we just need to resize the.ancestor font without affecting other elements.

It’s not hard to see by the time you get to this point, but the problem with styling is that it’s too easy to influence other components, too easy to be influenced by other components. The question most people have is:

  • I thought I was changing the style of component A, but I was implicitly affecting component B
  • Component A is affected by several sets of styles at the same time, and no one can modify it individually to achieve the final result

The solution to this problem has been around for a long time, and that is style isolation. For example, Angular does this by adding random attributes to elements and attaching attribute selectors to styles. For example, you create both the page-title component and the section-title component, both of which have the h1 element style, but when compiled you see CSS like this:

h1[_ngcontent-kkb-c18] {
    background: yellow;
}

h1[_ngcontent-kkb-c19] {
    background: blue;
}
Copy the code

This way all h1 element styles are not affected by each other

Problems in implementation

Pre-Processer

No matter how much you subjectively want to avoid all of the above, give styles a good clean architecture. In the process of implementation, we still accidentally fall into tool traps.

Back to the popup style we mentioned above:

.popup {
  width: 100px;
  height: 30px;
  
  background: blue;
  color: white;
}
Copy the code

If you find {background: blue; color: white; } is a common style that comes up a lot and you want to reuse it, but when programming with Sass it’s obvious that you have two choices: @mixin or @extend.

If a mixin is used, the code is as follows

@mixin common {  
  background: blue;
  color: white;
}

.popup {  
  @include common;  
} 
Copy the code

If extend:

.common {  
  background: blue;
  color: white;
}

.popup {  
  @extend .common;  
}
Copy the code

The first problem is that no matter which model you choose, it’s hard to tell whether developers are intentionally relying on abstraction or implementation. We can read @mixin common and.common as encapsulation of an abstraction, but it’s likely that future consumers will simply want to reuse background and color. Once this happens, the Common module becomes difficult to modify, because changes to any one property affect unknown modules.

In SASS, we can add parameters to class names and pass them as parameters, but it is not the same as variables and functions in real programming: in JavaScript functions, we usually only care about their input and output, and just defining the function does not affect the results of the program. The moment you define the style class, you can already have an impact on the page, and every attribute in it will have an impact.

If you have heard the phrase “composition is better than inheritance”, I am sure you will have a deeper experience of this. You can recall the side effects of inheritance, such as inheritance breaking the encapsulation of the superclass, subclasses can not reduce the interface of the superclass, and so on. SASS can find similar reuse relationships.

What makes Extend even more dangerous than mixin is that it breaks the way we organize our modules as usual.

For example, there is a page page with a set of page-title styles:

.page {
  .page-title {
      .icon {
          width: 10px;
      }
      
      .label {
          width: 100px; }}}Copy the code

Now card-title wants to override it with extend:

.card-title {
    @extend .page-title;
}
Copy the code

The result will look very strange after compiling:

.page .page-title .icon..page .card-title .icon {
  width: 10px;
}
.page .page-title .label..page .card-title .label {
  width: 100px;
}
Copy the code

Even if you haven’t heard of BEM, your programming experience should tell you that page and card styles belong to different modules. But in fact the result of compilation is more like prioritizing reuse, forcing the two to be coupled together in cross sections.

And if you try to abstract the common title style into mixins and reuse it in page-title and card-title:

@mixin title {
    .icon {
        width: 10px;
    }
    
    .label {
        width: 100px; }}.page {
    .page-title {
        @include title        
    }
}

.card-title {
    @include title
}
Copy the code

The result of compiling is as follows:

.page .page-title .icon {
  width: 10px;
}
.page .page-title .label {
  width: 100px;
}

.card-title .icon {
  width: 10px;
}
.card-title .label {
  width: 100px;
}
Copy the code

Obviously the page and card styles are more distinct

An Necessary Evil

If you ask me if I will abide by each of the principles I wrote above, my answer is no. In practice I tend to trade convenience for maintainability.

The only constant in the world of programming is change itself. No matter how well you’ve done your object-oriented design and how well you’ve split components before you hit the keyboard, any change in the business has the potential to make all of your designs go back to square one. So to ensure that the code accurately reflects the rationality of the business knowledge, we need to redesign the code design from time to time.

As you can imagine, the whole process involves revisiting the architecture, reading and understanding the code from the ground up, and validating the changes once they’re done. This sequence of steps is costly, not to mention the trial and error involved and the wasted opportunity to add new functionality due to refactoring. More importantly, the costs are there, but the benefits are not.

If your style code is based on the Design System, your change costs will be higher. You’re less likely to change the code from your personal point of view, and more likely to use the entire product’s design language to justify the change from top to bottom.

Another, more practical issue is that code is never maintained by an individual. When there is no consensus on this theory within the team, or everyone only knows about it in theory but does not care about it in practice, the painstaking efforts of a few people will eventually come to nothing. Code should ideally eliminate the “human” factor to the greatest extent possible and become an industrial product on an assembly line. So when I find that a framework requires people to read dozens of pages of best practices to write good code that meets official standards, the chance of good code in the real world is zero — a valid ESLint rule is better than ten pages of spec output. The principles described in this paper fall into the latter category.

But what if CSS code is written in a mess? It’s true that the product is broken, but the interesting thing compared to other bugs is:

  • A higher probability of finding style problems than scripting, what you see is what you get.
  • The damage is less than scripting, and the product is still usable
  • Problems can be fixed at low cost, and quick fixes can be targeted without even needing to read the full source code

Based on the above three points, and considering the complexity of the current technology stack, high learning costs, script development workload, and heavy delivery pressure, the correctness of the style architecture is inevitably the one to be sacrificed.

Finally, I would like to reiterate that I do not encourage such behavior, it is only a possibility under the pressure of reality. That’s fine if you’re in a well-resourced project and people are determined to get things right.

Functional CSS

There are other practices that I think are outside of this system, such as Tailwind and Tachyons. They are called “functional” styles because instead of providing componentialized, semantic styles such as.card,.btn, these frameworks provide “utility classes” such as.overflow-auto,.box-content, They are similar to pure functions without side effects in functional programming. When you need to add a style to your element, simply add the class name to the element:

<div class="overflow-auto box-content float-left"></div>
Copy the code

This practice deviates from the above architecture because it breaks the premise I mentioned above: there is a dependency between styles and DOM structures. In this programming mode, because there is no “cascading” relationship, each element’s style is independent of each other.

This pattern is heaven, and all the problems mentioned in this article are avoided: superelement dependencies, role coupling, and tangled reuse in the preprocessor.

But if you think about it, isn’t it pretty inline style? Inline style solves all of these problems as well. Are we back to square one?

In addition to the above issues, the reason I don’t give further recommendations or objections is that, on the one hand, this practice is highly controversial. On the other hand, I lack experience with such frameworks. The criteria for experience here is not “use”, but “long-term commitment to large collaborative projects” — the key words are “long-term”, “multi-user” and “large”. Because when we choose the technology, we have to consider the fit of the existing project, the cost of the team to adapt, and evaluate whether it will bring us great benefits in the long run to justify the cost of replacing it. These are the experiences I lack.

This article is also published simultaneously in The Front-end technology Hitchhiker’s Guide, welcome to follow