Comment Your CSS: Relationships

In this post, I will demonstrate situations in which we create relationships between rules in our CSS, before arguing for more discipline when authoring CSS to make such relationships explicit in our code.

Resets

A DOM element can have multiple rules as the result of four different design patterns:

  • When the HTML is configured, classes can be composed together on a single element, or the way the HTML is configured to create a DOM tree could result in elements inheriting styles from rules applied to parents.
  • At runtime, media queries can augment elements with additional rules.
  • When the CSS is authored, class inheritance results in an element having multiple rules.

When an element has multiple rules, conflicting style declarations between these rules can result in style resets. The following examples demonstrate how style resets can occur when an element is rendered with multiple rules.

Class composition

.widget {
    border: 1px solid black;
    padding: 1rem;
}

.widget--alt {
    border: initial;
}

<div class="widget widget--alt"></div>
The reset, as seen by Chrome DevTools’ style inspector

In this example, when these classes are composed together on an element, .widget-alt resets border back to its initial value, initial. Thus, there is a relationship between .widget and .widget--alt.

(I have used BEM as an example of class composition because it is a realistic example given the best practices of the day.)

Style inheritance

body {
    text-align: center;
}

.widget {
    border: 1px solid black;
    padding: 1rem;
}

<body>
    <div class="widget"></div>
</body>
The inherited style, as seen by Chrome DevTools’ style inspector

Style inheritance means that elements can inherit some styles from rules applied to parent elements. In this example, the div.widget element will inherit a text-align style from the parent body element.

In many ways, style inheritance is a powerful feature of CSS because it allows you to easily scope styles. For example, we could use style inheritance to give the whole page a font-family.

If we don’t want to inherit a style that is by default inherited, we must opt-out. In the following example, .widget resets text-align back to its initial value, initial. Thus, there is a relationship between the .widget and body rules.

.widget {
    border: 1px solid black;
    padding: 1rem;
    text-align: initial;
}
The reset, as seen by Chrome DevTools’ style inspector

Because of style inheritance, .widget could be penetrated with other styles depending on the DOM configuration, forgoing any hope of encapsulation. Why should .widget care about styles that it might inherit if it is used in a specific position in the DOM (in this case, a child of the body element)? With the addition of the text-align reset, .widget makes assumptions about what styles will appear on a parent somewhere. A truly ecanspulated class would not have to care about its environment. Our CSS shouldn’t need to know about how the HTML will be configured, because classes should be work whereever they reside in the DOM hierachy.

Fortunately, the forthcoming Shadow DOM features real encapsulation for CSS, requiring explicit opt-in to style inheritance (instead of having to explicitly opt-out like we do today).

Media queries

.widget {
    border: 1px solid black;
    padding: 1rem;
}

@media (min-width: 500px) {
    .widget {
        border: initial;
    }
}
The reset, as seen by Chrome DevTools’ style inspector

Media queries can augment elements with additional rules at runtime. In this example, .widget resets border back to its initial value, initial. Thus, there is a relationship between the two .widget rules.

Class inheritance

.heading-1 {
    font-size: 2rem;
    text-decoration: underline;
}

.page-title {
    @extend .heading-1;
    text-decoration: initial;
}

<body>
    <div class="page-title">Home</div>
</body>

Many CSS pre-processors enable fake class inheritance with the help of some sugar syntax. (I’m using Sass here; others may differ slightly in syntax.) Our pre-processor transpiles this by extending the .heading-1 ruleset to add the .page-title selector. For reference, the output CSS we are really interested in looks like this:

.heading-1,
.page-title {
    font-size: 2rem;
    text-decoration: underline;
}

.page-title {
    text-decoration: initial;
}

<body>
    <div class="page-title">Home</div>
</body>
The reset, as seen by Chrome DevTools’ style inspector

As a result of class inheritance, subclasses are given two rules in the CSS. In this example, the .page-title subclass resets text-decoration back to its initial value, initial. Thus, there is a relationship between the two .page-title rules.

Positioning

Relationships can also occur between elements when a parent element creates a new positioning context or stacking context. This happens when you use a value of relative or absolute for the position style.

.hero-image {
    background-image: url(…);
    position: relative;
}

.hero-image__text {
    position: absolute;
    bottom: 0;
    left: 0;
}

<div class="hero-image">
    <div class="hero-image__text">Some text on top of the hero image</div>
</div>

In this example, .hero-image__text positions itself at the absolute bottom left of its current positioning context, which is defined by the parent .hero-image. Thus, there is a relationship between .hero-image and .hero-image__text.

Likewise, any child of a relative or absolutely positioned element that has a z-index will also have a relationship because this position setting creates a new stacking context.

Comment Your CSS

If we model these relationships in our head when we author CSS, why don’t we model them in our code? We’re likely to forget these relationships exist at all if they are not documented.

.widget {
    /* border: 1px solid black; */
    padding: 1rem;
}

.widget--alt {
    border: initial;
}

<div class="widget widget--alt"></div>

Imagine you revisit a BEM “block class” (in this example, .widget) to remove its border (now commented out). In this example, a relationship has been broken between .widget and .widget--alt. Previously the BEM “modifier class” reset the border style back to its initial value. It’s easy to see that we can now remove the border declaration from .widget--alt.

In a large codebase, it could be difficult to see when you have broken relationships. Furthermore, and as we have seen, relationships can exist in several other forms, which could be manifested like spaghetti across your codebase. In my experience, this can lead to stray CSS. Any change you make in CSS could deem other styles redundant.

CSS is write only. You need to know every possible configuration of HTML you will ever render to know when it’s safe to delete a rule or style.

Pete Hunt

The big problem with stray CSS is that, if left undocumented, it can self-perpetuate: you overcome the stray CSS with a quick fix by resetting it, yet resets were the problem that lead to stray CSS in the first place. Alternatively, you can manually challenge each style to test its prevailing relevance given the current usage, but this can only be done by CSS experts. I call this phenomenon “reset culture”, whereby resets are constantly hacked into the codebase as a quick fix.

The reset, as seen by Chrome DevTools’ style inspector

When I look at my CSS, I want to know why styles exist so I know what’s safe to delete or change. Developer tools can show us how our CSS plays together, but we can’t see it in our codebase.

I have long wished for an IDE to show me the result of my CSS in the same way browser developer tools do. For now, I’ve resorted to discipline in the form of commenting all relationships.

.widget {
    border: 1px solid black;
    padding: 1rem;
}

.widget--alt {
    // Reset: .widget
    border: initial;
}

<div class="widget widget--alt"></div>

If a relationship is broken in a future version of this CSS and the author forgets to clean up, anyone who does revisit the redundant style will see, because of the comment, that it is no longer needed. If the stray CSS was left undocumented, it would likely never be removed (unless manually challenged), adding bloat to the codebase and creating a culture of fear in the CSS whereby the solution is to always override unusual looking styles. The same is true for all the other relationships I demonstrated previously.

Ultimately, resets are a side effect of the cascade. We should ask ourselves whether the cascade works for or against us for the sort of websites (or web apps, rather) that we build today. I hope we will see more research into the relevance of CSS today.

Postscript

  1. In the resets section I used the initial and inherit keywords. This could be improved by using the unset keyword, which evaluates to inherit for styles that are inherited by default, and initial for all other styles. Unfortunately, unset was not widely supported at the time of writing.
  2. We will see more relationships between our rules as we begin to use CSS flexbox.
  3. Disclaimer: the CSS for this site is very old.