How to write CSS without going crazy

Eugenio Monforte

CSS is an easy language to learn, but it can become a nightmare if you don’t have a strategy to develop it. Here is mine, developed in 15 years of experience.

In fact, it’s a progressive strategy: it’s developed only if conditions are met that force us to move to another level.

My 5-step strategy

  1. Write semantic HTML
  2. Establish design tokens
  3. Start with utility classes
  4. Isolate patterns as components
  5. Add Sass + Tooling

It’s really simple, but I think it avoids multiple headaches by focusing on the core of each situation.

Let’s go for it.

1. Write semantic HTML

That is, define the elements in such a way that they really express what the content is: a header like <header>, navigation components like <nav>, headline hierarchies like <h1>, <h2>, <h3>, etc. You get the idea.

This allows you to have a clear idea of what you want to stylize or use as a structure. Semantics can give us clues about which elements can or should become components, for example when a <article> is repeated a lot.

Furthermore, by establishing which element we will put in the interface we are facilitating later decisions when writing CSS: if a local interaction element is a <button> and not a <a> (contrary to what React devs generally think, for whom everything is an <a> and not a <button>), the options for style will be one and not the other.

2. Establish design tokens

With the CSS custom properties (known as variables) we can establish certain values that are constant within a project. We generally call this set of constants tokens. For example:

Colors

The color palette established by an organization or product must be strictly respected, since it’s one of the fundamental characteristics of the brand. Each color chosen represents something in the UI.

Therefore, the color palette is one of the first candidates when it comes to establishing tokens in the design. As it doesn’t change regularly, it’s natural to extract the values in RGB, HEX or HSL format, and save them in variables that we will use later along the CSS architecture.

:root {
    /* Brand's primary color HSL values */
    --brand-primary:        hsl(77, 64%, 43%);
    --brand-primary-dark:   hsl(77, 64%, 25%);
}

.btn-primary {
    background: var(--brand-primary);
}

.btn-primary:hover {
    background: var(--brand-primary-dark);
}

Typography

As with colors, the fonts used in the design elements of a brand are constants.

We can define as CSS variables from the values of each typography chosen: to the main typography, through the set of types of typographies such as serif, sans-serif, monospace, etc.

It’s the same for the size scale between the different elements: paragraphs, headings (in different levels), articles…

:root {
    /* Brand's primary font */
    --brand-font-sans:  "Open sans";
    --font-sans:        var(--font-sans), -apple-system, system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --font-mono:        Consolas, "Lucida Console", "DejaVu Sans Mono", "Liberation Mono", Monaco, monospace;
    --font-serif:       Constantia, Lucida, "DejaVu Serif", "Liberation Serif", Georgia, serif;
}

body {
    font-family: var(--font-sans);
}

Space / Size units

Another important aspect of a design is the spacing between elements, whose hierarchical consistency is central.

Here enters the so-called “vertical rhythm”, which is responsible for defining the vertical space between the elements to ensure a pleasant reading.

It is interesting to have a base value, to work on multiples according to that measure.

:root {
    /* Size units */
    --space-unit:   0.25rem;
    --sm:           calc(var(--space-unit) * 2); /* 0.5rem */
    --md:           calc(var(--space-unit) * 5); /* 0.75rem */
    --base:         calc(var(--space-unit) * 4); /* 1rem */
    --lg:           calc(var(--space-unit) * 5); /* 1.25rem */
    --xl:           calc(var(--space-unit) * 6); /* 1.5rem */
    --xl-2:         calc(var(--space-unit) * 8); /* 2rem */
}

body {
    font-size: var(--base);
    line-height: var(--xl);
}

h2 {
    font-size: var(--xl-2);
}

p {
    margin-bottom: var(--lg);
}

A lot more

  • Opacity
  • Border radius
  • Media Queries
  • Shadows
  • Z-index
  • And more…

You can define many values as constants so you can you use it later. For example, define a range of opacities, border-radius or z-index.

3. Start with utility classes

When we need to start writing styles, it’s better to do it step by step, attending to the concrete needs we have. We don’t want to overthink what we will “possibly” need.

So, instead of defining components that we may not need, it’s better to focus on creating utility classes that have a single property with its determined value.

That is, instead of writing a special .footer class with a top margin of 20px:

.footer {
    margin-top: var(--lg); /* 20px */
}
<footer class="footer">
    Footer content
</footer>

We define a .mt-lg utility class, which we can use anywhere else that needs a top margin of 20px.

.mt-lg {
    margin-top: var(--lg); /* 20px */
}
<footer class="mt-lg">
    Footer content
</footer>

4. Isolate patterns as components

If you’re repeating those same classes too much…

<article class="text-black text-base mb-sm">
    Article 1
</article>
<article class="text-black text-base mb-sm">
    Article 2
</article>
<article class="text-black text-base mb-sm">
    Article 3
</article>

Think if you need to define a component. That is, put all the repeated properties and values within a class that encompasses them, associating it to a recognized interaction pattern.

Only then, use a simpler BEM-like system like this:

/* Component */
.article {
    margin-bottom: var(--base); /* 1rem */
    font-size: var(--base); /* 1rem */
}

/* Component-child */
.article-heading {
    font-size: var(--xl-2); /* 2rem */
    color: black;
}

/* Component -modifier */
.article.-highlighted {
    margin-bottom: var(--xl); /* 1.5rem */
    font-size: var(--lg); /* 1.25rem */
}
<article class="article -highlighted">
    <h2 class="article-heading">Article heading</h2>
</article>

Variants

For components with similar but different structure or style.

Overwriting the base elements, we create variants either using utility classes or components with modifier classes.

/* Component */
.card {
    background-color: var(--bg-light);
    color: var(--text-dark);
    border-radius: var(--radius-sm);
}

/* Variant */
.card.-featured {
    background-color: var(--bg-blue);
    color: var(--text-light);
    border-radius: var(--radius-lg);
}
<!-- Component -->
<article class="card">
    <h2 class="card-heading">Card heading</h2>
    <p class="card-content">Lorem ipsum, dolor sit amet consectetur.</p>
</article>

<!-- Variant -->
<article class="card -featured">
    <h2 class="card-heading">Card heading</h2>
    <p class="card-content">Lorem ipsum, dolor sit amet consectetur.</p>
</article>

5. Add Sass + Tooling

If you’re establishing too many components, add Sass to divide the components into files and rely heavily on mixins.

This will force you to integrate a Sass compiler (like DartSass), a task runner (Grunt, Gulp) or a bundler (Parcel, Webpack, Rollup, you name it). As CSS becomes more complex, so will JS, and having tools to help us with that is always beneficial.

Split files

Although with CSS you can split the styles into several files and import them from a main file, the browser ends up having to make calls to each of those files.

With Sass, as it’s a preprocessor, this import from a main file is done before the compilation, so the final result is a single file. The preprocessing feature also allows us to minimize, compact or concatenate the code, which ends up achieving a lighter file.

/* Main.scss */
@import "variables";
@import "mixins";
@import "z-index";
@import "breakpoints";

Mixins

While Sass still has a lot of useful features, its usefulness has been greatly diminished by the great advance of CSS in recent years. Just for reference, CSS Custom Properties have replaced Sass variables.

Likewise, functionalities such as mixins still have enormous potential. For example, this mixin truncates text and adds an ellipsis to represent overflow.

@mixin ellipsis( $width: 100%, $display: inline-block) {
    display: $display;
    max-width: $width;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    word-wrap: normal;
}

.price {
    @include ellipsis;
}
/* CSS Output */
.price {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    word-wrap: normal;
}

Summary

  1. Write semantic HTML
  2. Establish design tokens
  3. Start with utility classes
  4. Isolate patterns as components
  5. Add Sass + Tooling

This small but powerful philosophy will allow you to have a clear perspective when structuring and stylizing a web app, avoiding getting stuck with common problems like the specificity wars or filling the files with similar styles.

Did it work for you? Share it with someone else.