DEV Community

akachicon
akachicon

Posted on • Edited on

A problem with variables in postcss

In postcss we have two primary methods of declaring variables: passing js object to compiler or using css variables of different flavors. Let's start with the css declarations.

CSS declarations

In postcss, thanks to the plugin system, we can use (at least) two different types of variables: dollar-sign variables and custom-property variables. The difference between the two is that the first are precomputed at the compile time, while the others are dynamic by nature (specification). It is worth noting that with help of plugins we can have the later precomputed in calc and media queries, so we can kind of treat these dynamic properties as static (in certain cases).

From the declaration point of view, while a dollar-sign variable can be defined anywhere in the file, custom property can only be defined as a property. In case of project-wide properties we usually define them on :root element. As for the compilation, postcss-css-variables plugin precomputes only those properties that are defined on :root selector, and the value of a property is taken from its last declaration.

Now, that we know our possibilities on declaring variables, let's think where we can use them. In postcss there is one (and only) way to combine css files - postcss-import plugin. As the plugin page says, this plugin should probably be used as the first of your list. This way, other plugins will work on the AST as if there only a single file to process, and will probably work as you can expect.

The statement above basically says that all that we have declared in the file imports is stored in the scope of that file. Now, let's consider one particular problem it leads to. In the example we will use dollar-sign variables and then we will describe the case for the custom properties.

Assume, we have the following two files:

/* a.css */
$some-var: 1rem;
Enter fullscreen mode Exit fullscreen mode
/* main.css */
@import '/path/to/a.css';

.main-class {
  height: $some-var;
}
Enter fullscreen mode Exit fullscreen mode

Over time, a new requirement comes, and now we have to add a new feature. Let's say it is a mixin supposed to be used in our main.css:

/* b.css */
$some-var: 2rem;
@define-mixin some-mixin {
    .mixin-class {
      height: $some-var;    
    }
}
Enter fullscreen mode Exit fullscreen mode
/* main.css */
@import '/path/to/a.css';
@import '/path/to/b.css';

.main-class {
  height: $some-var;
}

@mixin some-mixin;
Enter fullscreen mode Exit fullscreen mode

Now, we can see a problem. Because we used the same name - $some-var - in the mixin file, the value of the main-class height has changed. Someone could suggest we can solve this problem by using scoped variables but it won't work for a case with several mixins with a shared local variable in the same file. Unless we have a test for the case, it could be very hard to spot the bug, and even with the test, the origin of the problem is not instantly obvious.

In case of using custom properties nothing is different for the case. At first we have:

/* a.css */
:root {
  --some-var: 1rem;
}
Enter fullscreen mode Exit fullscreen mode
/* main.css */
@import '/path/to/a.css';

.main-class {
  height: var(--some-var);
}
Enter fullscreen mode Exit fullscreen mode

After adding some more code:

/* b.css */
:root {
  --some-var: 2rem;
}
@define-mixin some-mixin {
    .mixin-class {
      height: var(--some-var);    
    }
}
Enter fullscreen mode Exit fullscreen mode
/* main.css */
@import '/path/to/a.css';
@import '/path/to/b.css';

.main-class {
  height: var(--some-var);
}

@mixin some-mixin;
Enter fullscreen mode Exit fullscreen mode

It's worth noting, that with the custom properties approach an even more cunning bug could be introduced because for the precomputed value postcss-variables-plugin uses the latest declared value. This means, that we can accidentally change not only the result of the following code but of the previous as well.

One of the ways to solve the problem is to use some strict name conventions applying to all variables and possibly (I haven't tested it) other reusable entities. Such an approach would make code more verbose and, in my opinion, less pleasant to write.

JS declarations

Passing js object is good in that it could easily be shared with js code and we are sticking to the single source of truth principle. On the other hand, for each variable that is supposed to be local we should declare it in our global scope. This, again, leads us to the solution with some kind of naming convention, which would make code more verbose. Plus, for me, there is some inconvenience in separating a 'local' variable declaration from its usage.

Conclusion

It seems that right now postcss cannot provide both robust and convenient method to declare variables in files. And how would you solve the problem? Or maybe I'm missing something? Please, let me know in comments, I'm very interested in possible solutions.

Top comments (0)