~~New~~ Old way to write CSS
Tags: vue, eslint, css
Published: 2023-11-16 11:47:42

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uoi9rsdtnni2lddmy41k.png)

until they used on the same page...


![Components used together](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdpwiskc1g3o0pafakc0.png)

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](https://tailwindcss.com/), [WindiCSS](https://windicss.org/), [UnoCSS](https://unocss.dev/).

**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"](https://developer.chrome.com/docs/lighthouse/performance/unused-css-rules/) 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'](https://unocss.dev/transformers/compile-class) mode, which can compile several atomic classes into a single unique class.

UnoCSS can [generate](https://unocss.dev/integrations/vite#modes) 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.

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

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

## 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](https://github.com/vuejs/vue-loader/issues/957#issuecomment-329947476). The issue dates back to 2017, but this leak still remains as [by design](https://github.com/vuejs/core/issues/2669#issuecomment-747433501)
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](https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/enforce-style-type.html), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5fl87jz8zph4c27bnx1t.png)

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](https://github.com/zeokku/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](https://github.com/levchak0910/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sl7ld5fiam8n42sai6rn.png)

VKCN


![VKCN example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nqfuanokyjjuevvakgnk.png)

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](https://github.com/levchak0910/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ipz6bprpnb2tru1svng.png)

## 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](https://www.linkedin.com/in/levchak0910/).

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