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.
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.
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.
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.
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 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.
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.
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.
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.
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.
grid-cols-x
utility classes.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.
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.
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,
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.