How I Write CSS

The four categories of ruleset

This essay describes my method for writing Cascading Style Sheets, the styling language for modifying the presentation of pages on the world wide web. [1] I write CSS by hand in the uncomplicated environment of a text editor, so my method has evolved to minimize repetition, maximize readability, and ease editing. I try to write CSS with respect for the finite computing resources of end users. I try to write CSS that reads well and works well, using the language to its fullest potential. My method presumes a semantically clear and well-structured HTML document [2] as the starting point. It is worth taking care to create a meaningful document structure for a web page before styling it, because the semantics of HTML serve as the vocabulary for CSS.

Good styling communicates a structure the document already possesses. That structure both guides and eases our task. However if we are not careful, we can still do a poor job even starting with a well-structured HTML document. If we write CSS selectors that are unintentionally specific and ordered haphazardly then we cannot easily predict which is considered most relevant by the specificity algorithm, [3] nor where in our stylesheet the outcome is determined by the order of appearance of our rulesets. [4] If our selectors are too specific, our styling is brittle and liable to break with changes in document structure. If we misuse language features then inevitably we must write baroque rulesets to wrest back control, or else use overpowered selectors for everything and bloat our stylesheet to a ludicrous size.

Alternatively we can write CSS in such a way that specificity is crystal clear. This gives us freedom to organize our stylesheet as we prefer, and to modify our styling later without guesswork. Our CSS is flexible enough to cope with changes in our document structure. Other people are able to read our stylesheet and understand it with a minimum of effort. Web browsers render our CSS with speed and efficiency. We use the language fully but eliminate the unpredictability that arises from sloppy use. To write CSS that achieves these goals, we use four categories of ruleset. We build our document styling in stages and we do as much as we reasonably can using one category of ruleset before we move on to the next.

The four categories of ruleset:

  1. Primitives
  2. Exceptions
  3. Variations
  4. Deviations

Sometimes we might write a ruleset that does not fit neatly into a single category; this is absolutely fine. The purpose of using these four categories is to write efficient, effective, readable, and maintainable CSS. There is nothing wrong with aiming for an ideal and also being pragmatic when it comes to the implementation. Like the points of a compass, the categories let us set a course and maintain it, even though we may decide to go around a few obstacles in our path. I will now describe the four categories of ruleset and how they work together.

Primitives

Primitives are the most generic rulesets we use in our stylesheet. These will almost always be single element selectors: html, body, p, strong, em, and so on. Primitives can be made slightly more specific if desirable, by targeting elements based on their relative position in the document structure. By defining these primitives, we are setting the fundamentals of our styling whilst avoiding premature complexity. These rulesets determine the backbone of our presentation: the basic colour palette, visual hierarchy, text sizes and font families, margins, borders, and padding choices for our web page. They would hold true even if the structure of our HTML were to change entirely, and they have low specificity that we can override with ease. @font-face rules can be thought of as a type of primitive, and are best placed where they will not cause distraction.

Examples of primitives:

html {
  font-size: 20px;
}
body {
  font-size: 1rem;
  margin: 2rem;
}
p {
  margin: 1rem 0rem;
}
ul {
  list-style-type: disc;
}
ul ul {
  list-style-type: circle;
}

Exceptions

Exceptions are rulesets that override the primitives to apply contextual adjustments to our styling. We use exceptions to smooth the rough edges of our primitive rulesets as needed. Usually the selectors will be composed of two elements and a combinator, or one element combined with a pseudo-class. Exceptions can modify our styling with extreme brevity because our stylesheet contains only primitives at this stage: for example, we might use two simple rules to remove the top and bottom margins from the first and last elements inside every descendent element on the page. @media rules should be thought of as a type of exception. These blocks can be concise if we use the cascade to best effect, avoiding repetition by allowing order of appearance to determine the invariant declarations and redeclaring only those properties that do need to change in response to the media query.

Examples of exceptions:

hr + hr {
  display: none;
}
p:empty {
  margin: 0rem;
}
body *:first-child {
  margin-top: 0rem;
}
body *:last-child {
  margin-bottom: 0rem;
}
@media print {
  html {
    font-size: 7mm;
  }
}

Variations

Variations are rulesets that vary the styling of selected elements based on characteristics they all share. Elements might be selected for variation because of their location in the document structure, their class name, or an attribute they possess. We use variations to style elements systematically. With variations we can present visually distinct regions of our web page, such as a masthead or sidebar navigation; or we can apply prominent and consistent styling to elements having an important systemic function, such as taking the visitor to the next page in a series or displaying an error message. Variations allow us to build a complete design system that handles more complex presentational problems in a unified way.

Examples of variations:

nav ul {
  list-style: none;
}
div[role="alert"] {
  color: crimson;
}
a.prev_page,
a.next_page {
  font-size: 1.2rem;
}
div.gallery {
  display: flex;
}

Deviations

Deviations are used to style components in our presentation that are unique or very rare. These rulesets cause elements to deviate from the design system we have established so far; we might use deviations to customize components in order to make our styling more expressive, or to apply a one-off design solution where it is needed. Often these rulesets will target elements using ID selectors to ensure greater specificity than all our previously defined rules. Elements without a unique ID can be targeted with increased specificity by including an ancestor element in the selector that has an ID or class name. Deviations are powerful selectors and should be used sparingly for that reason. If we misuse them as a convenient solution to every styling problem, we risk undoing all that we have achieved.

Examples of deviations:

#mascot {
  width: 32px;
  height: 32px;
}
#banner {
  position: sticky;
  top: 0rem;
}
form.search input {
  width: 20ch;
  display: block;
}