DEV Community

Ryan Boone
Ryan Boone

Posted on • Originally published at falldowngoboone.com

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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",
          },
        },
      },
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode
  • CSS is the source of truth
  • Great solution if you're using Create React App
  • Webpack configuration could be a challenge
  • Aimed more at library authors than app developers
  • Non-standard CSS

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 {
  ...
}
Enter fullscreen mode Exit fullscreen mode
// MyComponent.module.js

import * as React from 'react';
import Media from 'react-media';
import { larry, moe, curly } as bp 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>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • CSS is the source of truth
  • Easy to implement in Create React App
  • Again, Webpack (meh)
  • Appears to be more of a dev-friendly solution
  • Allows you to share values between CSS modules
  • Non-standard CSS

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

  • JavaScript (or JSON) is the source of truth
  • Requires Sass
  • Easy to implement with node-sass, but it's deprecated1
  • You have to role your own compilation if you want to define custom functions in Dart Sass

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>
)
Enter fullscreen mode Exit fullscreen mode

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.

  • JavaScript (or JSON) is the source of truth
  • Good option for shared React component libraries
  • Requires JavaScript to apply styles (no JS, no styles)
  • Non-standard CSS

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;
}
Enter fullscreen mode Exit fullscreen mode
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>
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  • CSS is the source of truth
  • Context-specific (CSS updates custom properties live based on the context)
  • Easy to implement
  • Not supported in Internet Explorer 11, but you can sort of polyfill it
  • Can't use Custom Properties for breakpoints šŸ˜©
  • Standard CSS

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.Ā ā†©

Top comments (1)

Collapse
 
ohadbaehr profile image
OhadBaehr

Great post!