CSS: Why we need localized constants

There’s become a growing concern that, in general, CSS is broken. While it hasn’t been something that’s prohibited me from work, it’s becoming more apparent the more complex our application gets.

Scoped CSS is something quite a few projects are tackling. One that’s caught our eye is CSS Modules, where you can set the build to modules for css-loader and enable a local scope by default. CSS Modules has a really nice interface, one that promotes extending through composition, which lets you share styles across different selectors – even if they’re in separate files (composes: foo from "bar.css";).

Apart from locally scoped styles, composition allows you to easily trace back to the source. Preprocessors like SASS take a far more lax approach with @extend .foo. This is easy to trace if the file is static and singular – however if it imports other files, finding .foo (if you’ve got more than one, good luck!) is a global search task.

What CSS Modules fails to tackle in its “No global scope” is CSS variables – and for good reason. CSS Modules is tackling the scope of CSS selectors and declarations, not trying to redefine how CSS variables should work.

Why do CSS variables exist?

Preprocessors have had variables for years, and we’ve just assumed that we need them. The newish (official) spec on variables describes a familiar situation:

a site may establish a color scheme and reuse three or four colors throughout the site. Altering this data can be difficult and error-prone, since it’s scattered throughout the CSS file (and possibly across multiple files), and may not be amenable to Find-and-Replace

That sounds incredibly logical, and it’s a situation variables have been used in many, many times. You’ll often find a file that stores a bunch of colors, or more generally, a bunch of variables. Bootstrap and other css frameworks use variables in a slightly different manner – they use them as a default which is somewhat expectedly overwritten by the user.

Generally, though, you see variables assigned once and then used throughout the stylesheet numerous times, without being re-assigned. I have no stats to back that statement up, just experience, we’ve all done it. Most variables in CSS should be constants.

Local vs. Global

Preprocessors have both local and global variables, and the spec has the same intention:

:root {
    --primary: red; /* accessible everywhere */
}

.foo {
    --primary: blue; /* accessible if inherited from .foo */
}

Generally, we globalise variables. Especially in preprocessors, there’s very few reasons to declare a local variable and not want to access it outside of that block. Global variables are hard to manage and hard to diagnose.

Declaring variables under :root makes sense if you’ve got a single .css file (or even two, maybe three… but you get the idea). When you’ve got twenty stylesheets that are imported before they’re calculated, tracing a variable from the point of execution up to the point of declaration could get tricky.

A simple solution would be to ensure all variables are declared under the same :root in a variables.css file – it’s a single point of entry and easy to reference. This however, it’s not nice to maintain because it takes a blanket approach to storing variables. Having colors, dimensions, media queries, content references (and more!) in a single file for an application can get unwieldily. Separation of concern applies to sets of variables too.

CSS Constants

CSS constants don’t exist in a spec, they’re currently just variables that aren’t re-assigned. Whether CSS is a programming language or not, it results in a different environment for variables to live, compared to something like Javascript. CSS is a closed environment, everything is calculated without much external input. Sure, pseudo’s like :hover allow you to conditionally style, but it’s not really an external input (much less a variable input). There’s a limited set of information you can conditionally meet, like @media.

If we have a color scheme that has four colors, primaryBlue, primaryGreen, primaryOrange, and primaryYellow, there’s no real reason why these declarations should ever be reassigned. If you’ve got a context specific primary color, there’s probably a decent reason why you should have two different constants for the different contexts.

It’s easy to say you shouldn’t be reassigning colors based on context, but what about integer based values like width, padding or top? Again, because CSS doesn’t allow much external input when it’s compiled, it’s relatively easy to predict arithmetic before runtime. However, CSS variables still have a place. We still need variables, and the spec has a great example why.

PostCSS-local-constants

A plugin for PostCSS, postcss-local-constants aims to add constants to the CSS pipeline. While they’re not local to a selector, they’re not global. The constants are accessible from any point in any file, but only through an explicit import from the constants location:

~colors: "./theme.js";

.foo {
    background: primary from ~colors;
}

This follows the same syntax that css-modules requires for composoition. Rather than including the path in every reference, postcss-local-constants lets you set it at the top (or before the constant reference). It’s easier (and less likely!) to change multiple ~colors than change a file path in multiple places.

What does the declaration look like?

theme.js

module.exports = {
    colors: {
        primary: green,
    },
};

What about default values?

When you call postcss-local-constants, you can pass it a set of default constants. This is useful if you’ve got a CSS framework, or are interested in multiple themes:

postcss([
  localConstants({
    defaults: {
      colors: {
        primary: 'blue',
      },
    }
  })
])

Or better yet, abstract the defaults away:

postcss([
  localConstants({
    defaults: require('./default-theme'),
  })
])

Contribute

This is a work in progress, we’d love some feedback. Check out the repo.

About James -

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *