Getting JavaScript to Talk to CSS and Sass

Avatar of Marko Ilic
Marko Ilic on (Updated on )

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.