Bridging the CSS-JavaScript Divide

CSS and JavaScript have coexisted for over two decades, yet sharing data between them has always been awkward. Large-scale solutions exist, but for many projects the simpler path is to leverage the tools already at hand: CSS custom properties for runtime values and Sass variables for build-time data.

Working with Custom Properties in JavaScript

Custom properties were designed to work with the DOM, which means JavaScript can both read and write them directly. Setting a value is straightforward with setProperty:

document.documentElement.style.setProperty("--padding", 124 + "px"); // 124px

Retrieving values works similarly. Since custom properties are computed styles, you can pull them off any element using getComputedStyle. The key detail is scoping: variables defined on :root live on the html element, so that's where you query them.

getComputedStyle(document.documentElement).getPropertyValue('--padding') // 124px

For inline styles set directly in HTML markup, getPropertyValue provides another route to the same data:

document.documentElement.style.getPropertyValue("--padding'"); // 124px

Bringing Sass Variables into JavaScript

Sass is a preprocessor — it compiles to plain CSS before the browser ever sees it. That means Sass variables don't exist at runtime and can't be accessed like custom properties. To share them with JavaScript, you have to export them during the build step.

If your project already uses loaders, the setup is minimal. The configuration requires three modules that handle importing and translating Sass modules, as shown in this webpack snippet:

module.exports = {
 // ...
 module: {
  rules: [
   {
    test: /\.scss$/,
    use: ["style-loader", "css-loader", "sass-loader"]
   },
   // ...
  ]
 }
};

The mechanism that makes this work is the :export block inside your Sass file. It tells webpack which variables to expose, and you can rename them to camelCase or cherry-pick only what you need:

// variables.scss
$primary-color: #fe4e5e;
$background-color: #fefefe;
$padding: 124px;

:export {
  primaryColor: $primary-color;
  backgroundColor: $background-color;
  padding: $padding;
}

Once that's in place, importing the Sass file into JavaScript gives you direct access to the exported variables:

import variables from './variables.scss';

/*
 {
  primaryColor: "#fe4e5e"
  backgroundColor: "#fefefe"
  padding: "124px"
 }
*/

document.getElementById("app").style.padding = variables.padding;

The :export syntax has a few constraints worth remembering:

  • It must sit at the top level of the file, though it can appear anywhere within that scope.
  • Multiple :export blocks merge their keys and values into a single export.
  • If the same key appears more than once, the last occurrence in source order wins.
  • Export values can include any character valid in CSS declaration values, including spaces.
  • Values don't need quotes; they're always treated as literal strings.

Practical Uses: Breakpoints and Animations

This pattern shines when you need consistency between CSS and JavaScript. A common example is syncing breakpoints so matchMedia() in JavaScript and media queries in CSS never drift apart:

// 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 natural fit. Duration and easing usually live in CSS, but complex sequences often require JavaScript — and they should both reference the same timing values. A strip-unit function helps keep exported values clean and easy to parse on the JavaScript side:

// animation.scss
$global-animation-duration: 300ms;
$global-animation-easing: ease-in-out;

:export {
  animationDuration: strip-unit($global-animation-duration);
  animationEasing: $global-animation-easing;
}
// 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,
});

Sharing variables this way keeps related code DRY and gives JavaScript and CSS a single source of truth without upending how either language already works. While other techniques exist — such as passing data through JSON — this approach stands out for its low overhead. It asks nothing more than exporting a few variables at build time and reading custom properties at runtime.