DEV Community

Cover image for How to combine utilities and handwritten styles in Atomic CSS?
Ramazan Maksyutov
Ramazan Maksyutov

Posted on

How to combine utilities and handwritten styles in Atomic CSS?

Hello, dear reader!

My name is Ramazan, I'm a Frontend developer, an enthusiast and I love exploring the cutting edge of modern web technologies.

In this article we will discuss whether it's acceptable to use handwritten CSS within the Atomic CSS approach. Opinions on this topic vary. Some believe that the atomic approach should rely on solely utility classes, while others are open to combining them with custom handwritten CSS. I'll try to shed some light on this supporting my perspective with arguments.

As an example of an Atomic CSS framework I will use mlut, which I'm accustomed to - it provides all the necessary features for a developer. Thus, with it you can understand every aspect of the interactions between utilities and handwritten styles within Atomic CSS.

To fully understand this article, you need to know HTML and CSS, and nothing else. In any case, knowing something about Atomic CSS will make the reading process even easier. All examples written in mlut are explained using CSS, so you don't need to know this framework.

Alright, I can't wait to get started.

A dark secret of Atomic CSS

up to 10% of all styles in a project you will have to write by hand even while using Atomic CSS approach.

It often happens that using utilities is simply inconvinient: they can clutter the layout significantly making it unreadible and difficult to maintain. Next, you will figure out in which cases it is more appropriate to apply handwritten CSS.

Setting up a pallete and custom variables

A website's color palette can be quite extensive and a good practice is to define it using custom CSS properties. There can be a large number of such properties: various sizes, the color palette itself, standard spacings, and so on. If you try to define all of these using utility classes, the markup will become completely unreadable. And if the website has multiple themes on top of that, it's a lost cause. Let me show you an example of what defining a palette looked like when I tried to do it using only utilities:

<article class="...
  @:pfcs-d_-FastLeftBgc-rgba(30,58,138,0.4) @:pfcs-d_-FastRightBgc-rgba(30,64,175,0.6)
  @:pfcs-d_-FastLeftBgc-rgba(25,60,184,0.5)_h @:pfcs-d_-FastRightBgc-rgba(20,71,230,0.7)_h
  @:pfcs-d_-FastHeadC-rgb(190,219,255) @:pfcs-d_-FastTextC-rgb(142,197,255)
  @:pfcs-l_-FastLeftBgc-rgba(239,246,255,1) @:pfcs-l_-FastRightBgc-rgba(219,234,254,0.5)
  ...and 16 more such lines with different colors...
"> ... </article>
Enter fullscreen mode Exit fullscreen mode

Can you read and understand it? Even I can't figure this out at a glance anymore, even though I wrote it myself. Therefore, to make life easier for both ourselves and others, it's better to define the palette and some important global variables in a separate CSS file or in the input file containing the mlut config. Of course, there's no need to move all custom properties there - that would overload that file instead. CSS variables that are only used within a specific component or widget are better defined in the corresponding markup using utilities.

Animations

If you use any complex animations in your project, you'll also have to define them using handwritten CSS. Of course, there are ways to create basic animations, as in other frameworks - they might offer a few utility classes for that. But if you want to create something truly complex and interesting, you'll need handwritten CSS, whether you write it in the Tailwind config or in the input file of mlut.

In mlut, I used the following workflow:

  • I defined animation steps using @keyframes in the input file;
  • Then, in the markup, I used the An* utilities, which correspond to the CSS animation-* properties.

Here is an animation example:

<p class="And4s Ann-changeСolor">Animated paragraph</p>
Enter fullscreen mode Exit fullscreen mode

Into the input file you should add the following rules:

@keyframes changeСolor {
  0%, 100% {
    color:red;
  }
  33% {
    color: blue;
  }
  66% {
    color: black
  }
}
Enter fullscreen mode Exit fullscreen mode

And here are the styles generated by mlut:

.And4s {
  animation-duration: 4s;
}

.Ann-change-color {
  animation-name: changeСolor;
}
Enter fullscreen mode Exit fullscreen mode

But since we already have many at-rules in the syntax, why not just include @keyframes as well? Yes, that would be great, especially for quickly creating not-too-complex animations. However, even if such functionality is added, complex animations should still be written in the input file or a separate CSS file to avoid cluttering the markup.

Resetting helpers

mlut has built-in entities called helpers. Essentially, these are established utility CSS classes that are needed in almost every project. They are optional to include, but they are present in the default config. btn is an example of such a helper, which allows you to set base styles for a button. This class contains the following styles:

.btn {
  display: inline-block;
  margin: 0;
  padding: 0;
  line-height: inherit;
  text-align: center;
  text-decoration: none;
  vertical-align: middle;
  background: none;
  border: 0;
  cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

If you want to modify this helper, you can override it in the input file using regular handwritten CSS. The styles of the helper will then be extended (or overridden) according to standard CSS cascade rules.

Styles from JS libraries

There are many libraries that help style markup using JavaScript. For example, the highlight.js library for syntax highlighting. For it to color the syntax correctly, you need to apply the appropriate class to the code block. And if you want to further modify these code blocks, you can define additional properties for the library's classes, such as .hljs or .language-html in the same input file. This way, for instance, you can remove scrollbars from code blocks.

How to relate utilities with handwritten CSS?

We'll discuss this using the features of the mlut framework. Simply put, you can use utility values in handwritten CSS. In mlut, this is handled by the apply mixin and the uv function, which we'll talk about next.

The apply mixin

In its standard use case, you can pass a list of desired utilities into it, and mlut will write the corresponding styles into the final CSS file. Here's roughly what it looks like in practice:

// input.scss

@use 'mlut' as ml with (
  // mlut config
);

.box {
  @include ml.apply("-Sz50vh Bgc-red M-a");
}
Enter fullscreen mode Exit fullscreen mode

This will lead to the following styles in the resulting CSS:

.box {
  width: 50vh;
  height: 50vh;
}

.box {
  background-color: red;
}

.box {
  margin: auto;
}
Enter fullscreen mode Exit fullscreen mode

As the example shows, if you use apply inside a selector, the generated styles in the final CSS will apply to that selector. If apply is used outside of any selector, the passed utilities will be generated as standalone rules.

You can also notice that for each utility used within apply mlut creates a separate rule with our selector .box. This is normal. For production styles are typically minified, and the minifier will automatically merge all these rules into one. The mlut CLI has a built-in minifier, which can be enabled in the config or via command-line arguments.

What apply is for

The main purpose of this mixin is to align handwritten CSS styles with utilities used in the markup. For example, you might have set margins for some blocks using utilities in the markup and now want to align them with the margins of elements styled with handwritten CSS due to the specifics of an external library.

The mlut framework has built-in measurement units like u and gg that are often used for developer convenience, but manually converting them precisely to standard units can be cumbersome. Furthermore, mlut has standard breakpoints for media queries, such as md, which corresponds to 768px and can be customized.

If you want to write media queries in handwritten CSS quickly without constantly checking breakpoint definitions, the apply mixin is very handy here. By the way, mlut has a dedicated mixin bp that also makes it easier to create media queries with breakpoints, but apply is more universal in this sense - it can be used for any at-rules.

Here is an example of using the apply mixin in this case:

.input-caption {
  @include mlut.apply("@s:acnc-a@:pfcs_C-red");
}
Enter fullscreen mode Exit fullscreen mode

CSS:

@supports (accent-color: auto) {
  @media (prefers-color-scheme: dark) {
    .input-caption{
      color: red;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Can we reuse styles with apply?

Theoretically, yes, it's certainly possible, but in reality, it's a poor solution. Any serious project today uses some form of markup generation. It's much better to use here so called aliases. In this approach one can create a global dictionary where the keys are aliases and the values are recurring sets of utilities:

module.exports = {
  externalLink: "Txd-n Fns6u C-$accent900 C-$accent850_h",
  navLink: "Txd-n Fns5u C-$accent600 C-$accent550_h"
}
Enter fullscreen mode Exit fullscreen mode

Then, use this dictionary in the markup:

<a href="#" class="<%= it.css.navLink %>"> Home </a>
Enter fullscreen mode Exit fullscreen mode

The advantages of this approach are:

  • aliases exist only at build time — existing utilities are reused;
  • there are fewer of them than regular classes — (almost) no naming issues;
  • aliases can be edited in-place by replacing substrings.

Therefore, for style reusability using aliases is considered a good practice. However, this topic is quite extensive and deserves a more detailed exploration in a separate article.

Function uv

Although this function is somewhat simpler than apply, it is also very important. What it does can be understood from its name: uv stands for "utility value". You pass a utility as an argument to this function, it extracts the value from it, and that value can then be assigned to a property within a handwritten CSS class. It looks like this:

.spoiler {
  margin: ml.uv('M3gg;a;6u');
}
Enter fullscreen mode Exit fullscreen mode

CSS:

.spoiler {
  margin: calc(var(--ml-gg) * 3) auto 1.5rem;
}
Enter fullscreen mode Exit fullscreen mode

The scope of this function is a bit smaller than that of the apply mixin. It should only be used to align values between handwritten CSS properties and utilities in the markup, especially when dealing with complex values.

Conclusion

So, my article is coming to an end. In summary, we can conclude that using handwritten CSS within an atomic approach is acceptable in cases where you:

  • configure the color palette and custom properties;
  • create complex animations and decorative effects;
  • adjust the styles of various libraries and frameworks.

We also discovered that you can use utilities even within handwritten styles to keep property values consistent across the entire codebase. I hope you found this interesting and helpful!

In the future, I plan to write a few more articles with my thoughts on Atomic CSS and Frontend development in general. So, stay tuned!

Wishing you success on your exciting journey in Frontend development!

Top comments (0)