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",
},
},
},
],
},
};
- 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 {
...
}
// 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>
);
}
- 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
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.
- 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>
)
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;
}
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.
- 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!
-
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)
Great post!