DEV Community

Cover image for ~~New~~ Old way to write CSS
Levcsák Sándor
Levcsák Sándor

Posted on • Edited on

~~New~~ Old way to write CSS

Today I would like to write about CSS encapsulation and show you how we can write styles in an old way, but with better developer experience (DX).

But first let's start from what CSS encapsulation is and the problems it solves.

Imagine we have two components. For developing those two components responsible two different developers, who are working on them in parallel but their scopes are not overlapping with each other. Both components have a child element which should be perceived as a title, but for one element the title should have green background, for the other's title red text color should be used. So obviously both developers use class .title to express that this child element is a title.

While using them separately - everything seems to be ok

Components used separately

until they used on the same page...

Components used together

What is happening here is called cascading. It is not bad, it is not good, it just works.

The problem though is that our two child elements having the same meaning and identified by the same name. It is a development process problem, not a technology problem. And the solution which can solve this problem is called - CSS encapsulation.

There are different approaches how we can encapsulate our styles, so they don't leak from one component to another:

  1. Naming conventions
  2. Atomic CSS
  3. Framework specific
  4. CSS modules
  5. Shadow DOM
  6. CSS-in-JS
  7. CSS @scope at-rule
  8. and many more ...

All of them have their pros and cons, but I would like to focus on first three in this article.

Let's start from

Atomic CSS

The Atomic CSS approach represents a method in which we create a single class that only does one thing. In essence one class represents a combination of property(ies) and a value. Which means we have predefined set of classes and to get a final state of the element we need to compose those classes.

For example:

flex = display: flex;
mr-1 = margin-right: 8px;
py-2 = padding-top: 16px; padding-bottom: 16px;

The most popular tools that implement this approach are: Tailwind, WindiCSS, UnoCSS.

Pros:

  • 100% protection from style leaks
  • easy to write code (class names are easy to remember)
  • highly customizable config and well designed API
  • there's no need to switch between files/sections to understand how DOM element will be styled
  • only used classes will end up in bundle
  • has tooling support (eslint plugin, vscode extension)

Cons:

  • hard to read code (single DOM element can contain 10-20+ classes)
  • inconvenient devtools experience (there no easy way to toggle a class only for 1 DOM element)
  • can not cover all CSS usage scenarios (even with arbitrary values)
  • HTML output became more verbose (what make the final bundle bigger)

And one another problem of this approach

...which (almost) nobody is talking about.

The final CSS file contains all classes used in whole application. While it is good from one point of view (because we don't repeat combinations of property and value), it also bad from another - on the first load we download the whole file, but on this exact page only a small percentage of classes are used.

Mostly, the percentage of unused styles is close to 90% and becomes even higher when the application has lots of pages and especially when lots of classes are used with arbitrary values. This disadvantage increases total download time, time to the first content paint (FCP) and causes "Unused CSS warning" in the Lighthouse report.

For example on the main page of the official Tailwind site: the bundled CSS file contains 48.7Kb of code and only 10.2% is used (as of the time of writing).

Some thoughts

1 - From my point of view Tailwind is good in places where FCP is not so important (like an internal SPA, hidden behind authorization) and not really good choice for an SSR application where FCP matters.

2 - From my point of view, Tailwind is good for a project where the design is homogeneous and not a really good choice for a project with a very custom and diverse design.

Interesting approach

UnoCSS has the so-called 'class compile transformer' mode, which can compile several atomic classes into a single unique class.

UnoCSS can generate classes into components as Scoped/Module CSS instead the final bundle.

And it is possible to combine these two features. It solves big final output bundle problem and provides better DX with devtools.

import { defineConfig } from 'vite';
import unocss from 'unocss/vite';

export default defineConfig({
  plugins: [
    unocss({
      mode: 'vue-scoped',
      transformers: [ transformerCompileClass() ],
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Framework specific tooling

Here I will mention tooling only within the context of Vue

When talking with a Vue developer about CSS encapsulation the first idea that comes to mind is

Scoped CSS

Scoped CSS is a great feature which provides you almost complete prevention of style leaks between components. Well almost means you should remember several caveats with scoped styles:

  1. having same class from parent component on root DOM element of child component will lead to style leak. The issue dates back to 2017, but this leak still remains as by design
  2. the whole project should use scoped styles everywhere and should not use globally defined styles, because scoping is based on adding data-v-{id} attributes to all DOM element and CSS classes within component, but DOM element still can be matched by a class from the global scope. Usage of scoped style across application can be guaranteed by eslint rule, but prevents leak from global CSS scope is almost impossible

Another minor issue is flooding HTML and CSS files with data-v-{id} attributes which inflates final bundle, what increases FCP.

But despite few obstacles, Scoped CSS provides really great DX. The code is written absolutely naturally and easy to read, which is not possible to say about

CSS Modules

CSS Modules - it is a method that exposes the resulting CSS classes from styles to the component as an object under the key of $style

While the way a code is written is weird verbose...

Example with Vue CSS Modules

emitted final bundle shows the best result: no flooded HTML and CSS, class names are obfuscated so small in size, which also makes it harder to parse the site and it is fully compile time only (unlike scoped). But DX is unpleasant, really (

"I have a dream"

Imagine we can combine advantages from both approaches: having the same DX as with Scoped CSS and having the same bundle output as with CSS Modules. Well now this dream is a reality, welcome vite-plugin-vue-css-modules

The plugin statically processes and replaces names, so there's also no scripting overhead due to accessing the $style object.

No more words needed, it is just amazing, give it a try )

Naming convention

Probably the most popular way to encapsulate styles. This approach implies using class names in a negotiated way, that they will not intersect with each other. There are different conventions, the most popular is BEM (Block, Element, Modifier).

While naming conventions may solve leaks problem, they are quite hard to manage. Your team should spend a lot of effort to keep this convention correct. On individual level each team member should know all aspects "how to" name classes and be mentally comfortable with it.

The biggest problem with naming conventions though is absence of tools that helps to automatically avoid mistakes.

And today I would like to present such a tool

VKCN

@vkcn/eslint-plugin is an ESLint plugin which validates that CSS code written in Vue SFC satisfies "vue kebab naming convention". It was designed with first-class support for Vue projects.

This convention enforces using only one unique class name per one or more DOM elements across whole application. It can be achieved with a similar approach to BEM.

Class names should follow pattern <prefix>--<element>, where:

  • prefix - used to give a scope for all elements used within one component, which usually equals to the component name or can be overwritten by a special comment, should be written in kebab-case
  • element - any custom name which appropriately describes DOM element, should be written in kebab-case

Like in BEM, prefix same as block, element has equal meaning, but has different delimiter: VKCN uses double hyphen (--), BEM double underscore (__).

State of the element can be extended with modifiers and attributes:

  • &[disabled] - for attributes
  • &.active - for states which doesn't have attribute

Modifiers are especially handy when writing Vue templates compared to BEM, where modifiers should include block (and element) name and should be separated by double hyphen (--) delimiter.

Compare these examples:

BEM

BEM example

VKCN

VKCN example

All classes are defined in the global scope follow the pattern <prefix>--<element> and it is safe to use any name in kebab-case for modifiers and no need to worry that styles will leak somewhere.

VKCN advantages over BEM

  • less code to type
  • easy to copy the class name and search across whole project
  • no dead code left when styles were removed

Other then ensuring class name parts are written in kebab-case with correct delimiters, this plugin:

  • restricts what can be nested
  • prohibits using dynamic classes
  • suggests to use defined classes in template
  • automatically sets prefix for all classes

Still, the main problem hasn't been resolved

Classes can be defined more than once in different files. Here ESLint unfortunately can not help, because it works only with one file. So for that another tool was created - @vkcn/reporter. It checks that no identical classes satisfying the vue kebab naming convention are defined in multiple files (currently .css, .scss, .vue are supported).

So as mentioned in the beginning of the article - DX we can have

In this example, I made several mistakes when typing class names. On the left side, you can see how it will look like on the default setup - everything seems to be OK until you open the app in the browser and see that some elements are not styled. On the right side all errors are shown in the editor so you can fix them immediately.

VKCN DX example

Epilogue

There are lots of different approaches to prevent style leaks and all of them are good, you just need to select some that will be appropriate for your team and project.

This article looks a bit emotional. Most of the things are represented through my biased point of view and may not find your support, so let me know what you think about it in the comments.

Anyway, I hope you liked the suggested solutions and consider trying them in your working life. Also don't hesitate to give them a star )


P.S. 1: I am open to new opportunities and interested in SSR-heavy projects written in TS/Vue/Nuxt/Qwik, so if you are developing such - feel free to contact me.

P.S. 2: Thanks to my friends Marton and Marcell for their help in improving this article.

Top comments (4)

Collapse
 
lixeletto profile image
Camilo Micheletto

There some stuff you can consider in your approaches.

Tailwind have great JIT and compression, it can generate oversized bundles for small applications, but it has an O(logN) growth as the application scales and the CSS usage stabilizes. Still, not the DRYiest approach and one of Tailwind's tradeofs.

As your BEM example, there is no rule in BEM website that says you can't mix BEM classes and utility classes. If your Modifier class refers to an specific property and not to a whole new variant, it should be an utility. In the example the block naming spams more than 20 characters, this may be an code smell for an overspecific | broad block application. For this issue one can consider the CubeCSS approach and it's vision over layout composition.

About the .title problem, Tailwinds solves this with variable composition. Adam Wathan wrote about this in his article:

You can even inject modifying variables in style attribute without the specificity downsides of inline CSS and with default values.

<p class="title" style="--color: var(--greenwash);"> ... </p>
Enter fullscreen mode Exit fullscreen mode

 

.title {
  color: var(--color, tomato);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ingosteinke profile image
Ingo Steinke, web developer

If developers have access to the complete repository, stylelint and integration tests should check the final result, not only the part that some dev is currently working on. Even if we build this only for test + linting purposes, we can import all component styles into one integrated CSS build file and let stylelint check that and warn about duplicate class names, specificity problems etc.

Collapse
 
levchak0910 profile image
Levcsák Sándor • Edited

Can you please explain a bit more about how integration tests can help to prevent style leaks?

Stylelint works in the same way as ESLint. It doesn't have access to the whole project, it runs rules across a single file and doesn't know anything outside that file.

we can import all component styles into one integrated CSS build file and let stylelint check that and warn about duplicate class names

That is almost exactly what vkcn-report-duplicated-class-selectors do - it collects all classes from all files in the project, groups them, and reports duplicates (it can not do other things and it should not)

Collapse
 
ingosteinke profile image
Ingo Steinke, web developer
@import foo.css;
@import bar.css
Enter fullscreen mode Exit fullscreen mode

maybe you're right and stylelint still doesn't know but now the IDE does.