JavaScript and CSS have lived beside one another for upwards of 20 years. And yet it’s been remarkably tough to share data between them. There have been large attempts, sure. But, I have something simple and intuitive in mind — something not involving a structural change, but rather putting CSS custom properties and even Sass variables to use.
CSS custom properties and JavaScript
Custom properties shouldn’t be all that surprising here. One thing they’ve always been able to do since browsers started supporting them is work alongside JavaScript to set and manipulate the values.
Specifically, though, we can use JavaScript with custom properties in a few ways. We can set the value of a custom property using setProperty
:
document.documentElement.style.setProperty("--padding", 124 + "px"); // 124px
We can also retrieve CSS variables using getComputedStyle
in JavaScript. The logic behind this is fairly simple: custom properties are part of the style, therefore, they are part of computed style.
getComputedStyle(document.documentElement).getPropertyValue('--padding') // 124px
Same sort of deal with getPropertyValue
. That let us get the custom property value from an inlined style from HTML markup.
document.documentElement.style.getPropertyValue("--padding'"); // 124px
Note that custom properties are scoped. This means we need to get computed styles from a particular element. As we previously defined our variable in :root
we get them on the HTML element.
Sass variables and JavaScript
Sass is a pre-processing language, meaning it’s turned into CSS before it ever is a part of a website. For that reason, accessing them from JavaScript in the same way as CSS custom properties — which are accessible in the DOM as computed styles — is not possible.
We need to modify our build process to change this. I doubt there isn’t a huge need for this in most cases since loaders are often already part of a build process. But if that’s not the case in your project, we need three modules that are capable of importing and translating Sass modules.
Here’s how that looks in a webpack configuration:
module.exports = {
// ...
module: {
rules: [
{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"]
},
// ...
]
}
};
To make Sass (or, specifically, SCSS in this case) variables available to JavaScript, we need to “export” them.
// variables.scss
$primary-color: #fe4e5e;
$background-color: #fefefe;
$padding: 124px;
:export {
primaryColor: $primary-color;
backgroundColor: $background-color;
padding: $padding;
}
The :export
block is the magic sauce webpack uses to import the variables. What is nice about this approach is that we can rename the variables using camelCase syntax and choose what we expose.
Then we import the Sass file (variables.scss
) file into JavaScript, giving us access to the variables defined in the file.
import variables from './variables.scss';
/*
{
primaryColor: "#fe4e5e"
backgroundColor: "#fefefe"
padding: "124px"
}
*/
document.getElementById("app").style.padding = variables.padding;
There are some restrictions on the :export
syntax that are worth calling out:
- It must be at the top level but can be anywhere in the file.
- If there is more than one in a file, the keys and values are combined and exported together.
- If a particular
exportedKey
is duplicated, the last one (in the source order) takes precedence. - An
exportedValue
may contain any character that’s valid in CSS declaration values (including spaces). - An
exportedValue
does not need to be quoted because it is already treated as a literal string.
There are lots of ways having access to Sass variables in JavaScript can come in handy. I tend to reach for this approach for sharing breakpoints. Here is my breakpoints.scs
file, which I later import in JavaScript so I can use the matchMedia()
method to have consistent breakpoints.
// Sass variables that define breakpoint values
$breakpoints: (
mobile: 375px,
tablet: 768px,
// etc.
);
// Sass variables for writing out media queries
$media: (
mobile: '(max-width: #{map-get($breakpoints, mobile)})',
tablet: '(max-width: #{map-get($breakpoints, tablet)})',
// etc.
);
// The export module that makes Sass variables accessible in JavaScript
:export {
breakpointMobile: unquote(map-get($media, mobile));
breakpointTablet: unquote(map-get($media, tablet));
// etc.
}
Animations are another use case. The duration of an animation is usually stored in CSS, but more complex animations need to be done with JavaScript’s help.
// animation.scss
$global-animation-duration: 300ms;
$global-animation-easing: ease-in-out;
:export {
animationDuration: strip-unit($global-animation-duration);
animationEasing: $global-animation-easing;
}
Notice that I use a custom strip-unit
function when exporting the variable. This allows me to easily parse things on the JavaScript side.
// main.js
document.getElementById('image').animate([
{ transform: 'scale(1)', opacity: 1, offset: 0 },
{ transform: 'scale(.6)', opacity: .6, offset: 1 }
], {
duration: Number(variables.animationDuration),
easing: variables.animationEasing,
});
It makes me happy that I can exchange data between CSS, Sass and JavaScript so easily. Sharing variables like this makes code simple and DRY.
There are multiple ways to achieve the same sort of thing, of course. Les James shared an interesting approach in 2017 that allows Sass and JavaScript to interact via JSON. I may be biased, but I find the approach we covered here to be the simplest and most intuitive. It doesn’t require crazy changes to the way you already use and write CSS and JavaScript.
Are there other approaches that you might be using somewhere? Share them here in the comments — I’d love to see how you’re solving it.
This is awesome. Thank you very much.
This is big! Thanks for sharing.
Wow, I had no idea about the :export. Can be really useful. Thanks
Very useful, thanks
I use TailwindCSS in my pipeline so accessing a variable is as simple as just using a class from a design system or a reference to a theme object. The source for Tailwind config are json files, so those can be imported directly into js without any sass mumbo-jumbo
Thanks for sharing. Where I’m stuck is going the other direction: accessing JavaScript variables from SCSS. My client wants to define a theme in JavaScript and access theme values in SCSS.
I can find surprisingly little discussion of this. I tried the most promising Webpack-based technique from a 2018 article but couldn’t get it to work with recent libraries.
You can do it with css variables.
Javascript can assign css variables to any DOM node. Then you can you access it in your CSS.
Just wanted to also add that if you’re using Gatsby and want to export Sass variables with :export as shown above, that it will work in local development mode but then won’t in production for gatsby specific reasons. The simple solution is to name your file from
variables.scss
tovariables.module.scss
. Files named withmodules.scss
at the end are treated slightly differently.Some context: https://github.com/gatsbyjs/gatsby/issues/3607#issuecomment-504879759
Hi Marko, thanks for the informative article!
I’m trying to find official documentation for the “:export” syntax but I’m coming up short. Is this a webpack feature? Or a loader’s feature? If you could point me to official docs, I’d be grateful.
Thank you!
Hi Maxime,
This is enabled by
css-loader
. But is actually CSS-modules feature and here is the exact link to the export doc.Hope I answered your question.
For adding your script in webpage try this chrome extension
https://chrome.google.com/webstore/detail/website-scripting/aggnfbkmhedkekjoplldenefbchaoiln
For some reason if you write Jest tests and use :export the variables will be undefined when Jest runs.
This is exactly what I needed! I tried this but the
variables
object is undefined and does not contain the variables I defined in:export
in my scss file. Any help is greatly appreciated, thanks much!