Skip to main content
Home

Share variables between JavaScript and CSS

Whether you need site breakpoints for matchMedia or access to theme colors, sharing values between your JavaScript and CSS is sometimes unavoidable. The easiest solution is to copy and paste values, but how can you ensure values stay synchronized when that brand blue color changes to indigo?

The answer is to create a single source of truth by sharing values between the JavaScript and style layers. There are several ways to accomplish this, and the best approach for a project depends on its frontend stack. Here are all of the ways I know how to pass data back and forth between all the layers.

ICSS

CSS Modules, gives us two ways of sharing variables, the Interoperable CSS (ICSS) spec and the PostCSS Modules Values spec. ICSS appears to be the older of the two specifications, so I’ll start there.

ICSS is a low-level specification that’s mainly for loader authors. It describes how to treat CSS modules as JavaScript dependencies and introduces the :export directive to act as a way to export defined values. Coupled with Sass variables, it allows you to export theme values:

// colors.module.scss
// assuming this is within Create React App; the `.module` lets CRA know
// this is a CSS Module

$my-red: #ff0000;

:export {
myRed: $my-red;
}

The exported values are imported like much like any other JavaScript module:

// MyComponent.js
// assuming this is within Create React App

import * as React from 'react';
import * as colors from 'theme/colors.module.scss';

export function MyComponent() {
return <p>myRed's value: {colors.myRed}</p>
}

The above should work in Create React App out of the box. If you are rolling your own Webpack configuration (may God have mercy on your soul), you’ll need to configure modules with a compileType of icss:

module.exports = {
module: {
rules: [
{
test: /\.css$/i,
loader: "css-loader",
options: {
modules: {
compileType: "icss",
},
},
},
],
},
};

Setting @value

CSS Modules also offers the @value directive, which explicitly defines module values. @value can also be used to import values from other CSS modules. It’s a catchall solution for passing values to and from CSS modules to anywhere.

// breakpoints.module.css

@value larry: (max-width: 599px);
@value moe: (min-width: 600px) and (max-width: 959px);
@value curly: (min-width: 960px);

// MyComponent.module.css
// this is one of the multiple ways you can import @value definitions
// see https://github.com/css-modules/postcss-modules-values

@value larry, moe, curly from "theme/breakpoints.module.css";

@media larry {
...
}

@media moe {
...
}

@media curly {
...
}
// MyComponent.module.js

import * as React from 'react';
import Media from 'react-media';
import { larry, moe, curly } from 'theme/breakpoints.module.css';

export function MyComponent() {
return (
<Media queries={{ larry, moe, curly }}>
{matches =>
matches.larry
? (
<p>Oh, a wise guy, eh?</p>
)
: matches.moe ? (
<p>Why I outta...</p>
) : (
<p>Nyuk nyuk</p>
)
}
</Media>
);
}

Sass compiler

Sass’s JavaScript API can add custom functions by defining the functions option of render. You can use this to define getter functions that return your theme’s values directly in Sass. I implemented this on our website using node-sass, which exposes the functions option in its CLI:

node-sass src/styles/ -o build/styles --functions path/to/sass-functions.js

And the sass-functions.js file looks like this:

// sass-functions.js

const sass = require('node-sass');
const theme = require('../theme/index.js');

module.exports = {
'getColorMap()': function () {
return Object.entries(theme.colors).reduce(
toSassMap,
new sass.types.Map(Object.keys(theme.colors).length)
);
},
'getMqMap()': function () {
return Object.entries(theme.mqs).reduce(
toSassMap,
new sass.types.Map(Object.keys(theme.mqs).length)
);
},
};

function toSassMap(list, [key, value], idx) {
list.setKey(idx, new sass.types.String(key));
list.setValue(idx, new sass.types.String(value));
return list;
}

Note that I’m having to define Sass types. The getColorMap() and getMqMap() functions return Sass maps that include all of our theme variables. Very handy!

Unfortunately, LibSass, the core engine of node-sass, has been deprecated, along with node-sass. The canonical Dart version of Sass lacks a nice CLI option for custom functions. If you want to recreate this functionality, you’re stuck building a compiler using Dart Sass’s JavaScript API.

CSS-in-JS

A common variable sharing solution in React is to simply let JavaScript do all the work. And as controversial as CSS-in-JS seems to be, it presents an easy way to share variables simply because you are defining CSS in a JavaScript file.

Here’s how you might share a variable in the library Emotion:

import { css, cx } from '@emotion/css';
import * as colors from './theme.colors.js';

render(
<div
className={css`
color:
${colors.primary};
&:hover {
color:
${colors.secondary};
}
`
}

>

I get so emotional, baby.
</div>
)

I mean, it’s easy. It’s so easy that I’d debate whether or not this is classified as sharing variables between JavaScript and CSS, but I’m throwing it in anyway.

I’ve already mentioned Emotion, but other CSS-in-JS libraries to check out include Styled Components, JSS, Theme-UI, Radium, and Aprhodite.

Custom Properties

If you need a lightweight, “proper” way to share variables between JavaScript and CSS, look no further than Custom Properties. Custom Properties allow you to create arbitrary CSS properties and set them to any value you want.

:root {
--color-brand: #BADA55;
--color-secondary: #005DAB;
}
import * as React from 'react';

function MyComponent() {
const brandColor = getComputedStyle(document.documentElement)
.getPropertyValue('--color-brand');

return <p style={{ color: brandColor }}>I'm brand color!</p>
}

If you access these properties a lot, you may want to create a utility function to save on typing:

function cssValue(property) {
return getComputedStyle(document.documentElement)
.getPropertyValue(property);
}

Custom Properties are completely standard CSS spec, and this is the only solution that is dynamic, meaning properties can change based on context:

:root {
--color-brand: #BADA55;
--color-secondary: #005DAB;
}

@media (prefers-color-scheme: dark)
:root
{
--color-brand: white;
--color-secondary: #ccc;
}
}

When you access those properties, they’ll be different depending on the user’s preferences. This is incredibly powerful.

Lots of options

Sharing variables between CSS and JavaScript can help reduce toil and cut down on unintentional tech debt. And if you need to, you have no shortage of options. Just make sure you understand what you want to act as the source of truth and know your technical requirements.

If you found that helpful, please let me know by liking this post on DEV Community and sharing it. And if you want more articles like this, make sure you follow me on Twitter so you’ll know when I post new ones.

Until next time!


  1. Trust me when I say you should not use node-sass. It’s so buggy that it can’t process newer CSS. ↩︎