Home
Writing
3D Stuff
/

Writing CSS effectively

2022-05-14

One of the hardest languages to figure out has been CSS, unironically. Figuring out how to write it so that projects don't become unmaintainable has been an incremental learning process. So here are some observations / tips that may help in this area.




The condensed finding after writing multiple separate projects would be:

The less characters / lines of CSS you have to write, the better

Most of the recommendations listed below are a subsets of this rule. One explanation for why this works could be the fact that it's easier to maintain systems that are written in less lines. For example, it's easier to maintain a 100 line CSS file vs a 400 line CSS file.

Utility classes

In terms of characters of code written,

class="flex"

VS

.new_classname_i_had_to_invent {
  display: flex;
}

the first option wins. The result of both options is the same, but the speed of implementation differs quite vastly.


This is where utility classes come in. There are many great libraries which provide common utility classes, but tailwind seems to be the best option.



Speed is always a factor when creating frontends, and it seems like that's one of the metrics that's not mentioned quite enough when comparing tailwind vs not-tailwind approaches.


Thinking about the correct class name in situation x may not take too long, but those seconds pile up quickly.


Utility classes are great when you want to attach a couple of commonly used css properties to an element in a fast manner. Just like the time thinking about a new class for element x piles up quickly, the shortcut of using utility classes also reduces the amount of time needed to create new frontend designs. Time is money and tailwind saves time.

Limitations and root variables

Single point of reference should be the default for most projects. One way to implement this in pure CSS is by using root variables as much as possible.


Create root variables that hold values for the project (like bg-col-x, font-size-2 etc etc) and map them to the class with the same name. Use these variables / classes in the project as much as possible and avoid creating new values in 99% of the cases. One example of this is a small CSS helper library i've written that implements this.


Single point of reference allows us to have some structure for the project. If this is not done to some degree, things turn into a shitshow quite fast.

Darkmode

The goal is still to write as little CSS as possible. If :root variables are the baseline settings for the project, then implementing dark mode becomes much easier.


Tailwind does have utility classes that can deal with it, but dark:util-classanme means more characters of code written.


Taking this site as an example, the CSS responsible for the dark-mode is only 12 LOC currently, with no new darkmode specific classes used in the html.

[data-theme='dark'],
[data-theme='dark'] :root {
  --global-bg-col: rgb(16, 16, 16);
  --global-txt-col: rgb(243, 243, 243);
  --code-bg: #20222a;
  --code-txt: #f0f0f0;
  --border-1-1: 1px solid rgba(224, 224, 224, 0.6);
  --border-1-2: 1px solid rgba(232, 232, 232, 0.2);
  --border-1-3: 1px solid rgba(48, 48, 48, 0.08);
  --hr-1: rgba(208, 208, 208, 0.6);
  --hr-2: rgba(232, 232, 232, 0.4);
  --hr-3: rgba(221, 221, 221, 0.3);
}

For bigger projects this block would obviously be longer, but in terms of least characters written, this approach wins by a massive margin.

Text and Darkmode

One problem with this might be lighter shades of text, but that can be easily avoided by using opacity-x utility classes for making the baseline text a bit lighter. If --global-txt-col is the baseline for the text that is flipped during darkmode, then the reduced opacity text is still valid in darkmode.

Media queries

Media query breakpoints are a part of the project, so having a defined and limited set of breakpoints is also a must. From personal experience it always has felt like projects with definend and limited set of media breakpoints feel more in sync than projects that don't have them.


The question that arises from this is, How many breakpoints are needed? And that's a thing i've been figuring out lately. 4 is too little, 16 is too much. The number slightly depends on the scale of the project, so 9 breakpoints could possibly be enough for big ones, while 6 could be enough for small ones.

hide-on-x

There's an easy way to toggle elements based on screen sizes. If media queries hold additional classes, like this

/* md */
@media (max-width: 920px) {
  .hide-on-md {
    display: none;
  }
}

then all you need to do is attach that hide-on-md class to an element. This approach means that there is no custom css written for element x, resulting in less LOC.

grid layouts + hide-on-x

Lets say we want to hide header__mid container (a grid box with a width of 1fr) on md media breakpoint.

// css
.header {
  grid-template-columns: auto 1fr auto;
}

// html (pug)
div.header
  div.header__left
  div.header__mid some content that is too long from md breakpoint
  div.header__right

Attaching hide-on-md to header__mid class will break the content because the header__right will then have the width of 1fr. To avoid this, don't attach the hide-on-x class to the elements that hold the layout together. Instead, nest the content in one additional layer of divs, so that header__mid class would still preserve the 1fr width.

// html (pug)
div.header
  div.header__left
  div.header__mid
    div
    div.hide-on-md some content that is too long from md breakpoint
  div.header__right

With this approach no custom css is written, but the layout is still correct.

Global content containers

Here's a small snippet that shows how to implement page content layouts with optional full width backgrounds.

// css
:root {
  /* max-width for the main content */
  --max-width-1: 1325px;
}
.max-width-1 {
  width: var(--max-width-1);
}
.flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}
 /* padding for the content container */
.max-width-1 {
  padding: 0px 30px;
}

@media (max-width: 720px) {
  /* reduce content padding on smaller screens */
  .max-width-1 {
    padding: 0px 20px;
  }
}

// html
<div class="flex-center">
    <div class="max-width-1">content</div>
</div>

As the :root variable holds a predefined max width, tweaking it becomes extremely easy.

Dynamic grid children

An easy way to implement dynamic grids is using the following repeat() property

.homepage-grid {
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
}

This allows automatic grids with a predefined min-width + less conditional media queries that deal with sizing on smaller screens.

  • one situation where this approach should be avoided is when you need a limited width for grid children, if only one child is present. One example of this could be ecommerce shop product layouts. If only one product is present, it would take up the whole row, which might not be good, because the product picture quality should be higher then. In those situations, use the tailwind grid-cols-x utility classes.

Optimization

Image containers

Lighthouse scores take into consideration how much the content is shifting during the page load. Image elements without correct / predefined dimensions screw that score up a lot. One way to avoid that is to define the image container to the correct size before importing images in the layout. This means that the dimensions of the element that contains the image should not change while the image is loading.


One way to avoid this problem is by defining the dimensions of the container and importing the image at the end. This can be tested by commenting the source of the image in and out. If the dimensions shift while the image is toggled, then the problem is not fixed.

Purging CSS for SPAs

In some cases, all that is needed for purging css is a singe package.json line.

"scripts": {
  ...
  "postbuild": "purgecss --css dist/assets/*.css --content dist/assets/*.js -o dist/assets/  --safelist html body"
},

If this is added, every time you run npm run build, the postbuild script will also be triggered and purge the css in the dist folder.

  • note that you need to install purgecss as a dependency, if you're gonna do automatic builds for Netlify or other platforms

End Notes

I don't think that writing pure CSS classes will ever go away. There are cases where that's still a better option than using Tailwind.


For example,

  • if a class is used globally ( over multiple components ), it's still better to define it as a new CSS class. That way, if you want to tweak that custom class, you only have to change values in one place.
  • reusable buttons with hover and active states are also easier to implement as a new CSS class.

I don't really believe in this mentality that you have to write everything using Tailwind only. Seems like a crutch to me. Using a combination of tailwind and custom CSS seems to be the best option currently.