Thinking Out Loud

"Straight from the horse's mouth"
Billy Watson Senior Front End Developer

Harnessing the power of CSS variables

Dynamic, reactive and powerful; the var() is here to stay.


Variables. One of the fundamental bread-and-butters of programming; allowing us to write cleaner, DRYer, simpler and more flexible code. One of the big drawbacks in CSS for a long time was the lack of variables in the spec, which was arguably one of the largest selling points for the dawn of pre-processors like Sass, Less, Stylus et al.

However, these variables had a few crucial differences. They did allow a value (be it a colour, breakpoint or font-family) to be declared once and used throughout, but they always compiled down to their static original value. Once compiled, we have no more variable.


Enter CSS custom properties, or as they’re more commonly known: CSS variables. Though not a variable in the strictest sense, they’re the closest the standard spec has ever had. They’re actually not very new at all - having been around since 2012, but it was only four years later when Safari, Chrome, Android and Opera started support that they became feasible for use in the wild.

Following a standard key/value pattern - with an option to declare a default - they must always be named beginning with a double dash and are referenced using the var() function.


Normally set on the root, making them accessible to the whole document, they can also be declared further down in the DOM, adhering to the standard inheritance and cascading practices of CSS. They can be combined with (or used within) other CSS declarations, e.g. border: 0.2rem solid var(--grey);, and also inserted into the calc() function to allow even more flexibility.

They are also reactive, so the browser will respond to changes in the variable which can be very useful for animation purposes. They can help keep your code clean and lean, reducing the need for modifier classes and also making things more human-readable by extracting away complicated style declarations.

If you are using them for animation, always remember to take the necessary steps to minimise performance hit, such as leveraging requestAnimationFrame() and animating from/to opacity: 0.01; or opacity: 0.99; - as this avoids a browser repaint whilst still seeming silky smooth to the human eye.

The power of Custom Properties really becomes apparent when you combine them with JavaScript too; easily done using'--y', 30); (or $elem.attr('style', '--y: 30'); if you’re keeping things old school!) You can swerve a performance hit here too by keeping your values unitless and then converting them with calc(). e.g. if an element had style="--max-height: 92;" you could then read that in the CSS as max-height: calc(var(--max-height) * 1px);.

Due to their dynamism, we’ve utilised CSS custom properties in many applications at Absurd. We’ve used them in image detailing, for parallax animations and to add jazzy hover effects on mouse move. Also in more day-to-day roles, like to reduce general class bloat and boil the age-old accordion animation debate down into a few sexy lines.

Practical examples

Absurd workshop

Consider this simple example. When the user hovers the div, we are using a calculation to change the value of two variables, --x and --y, by 5px based on the position of the user’s mouse. These are used in the transform value of the pseudo element that contains the not-so-subtle box-shadow, making it look like the mouse is ‘pressing’ the shadow into the page on hover:

.workshop-photo {
  &:before {
    box-shadow: 0px 0px 5px 5px #999799;
    background: #999799;
    transform: translate(0, 0);
    transition: all 0.15s ease;
    will-change: transform;

  &:hover:before {
    transform: translate(calc(var(--x) * 1px), calc(var(--y) * 1px));
    transition-duration: 0.1s;

let $img = document.querySelector('[data-holder]'),
    needAnimFrame = true,
    pos = { x: 0, y: 0 };

const updateVars = () => {
  needAnimFrame = true;

  $'--x', pos.x);
  $'--y', pos.y);


const initHover = () => {

  $img.onmousemove = (e) => {

    let rect = e.currentTarget.getBoundingClientRect(),
        posX = (e.clientX - rect.left),
        posY = (e.clientY -,
        percentX = ((posX / rect.width) * 200) - 100,
        percentY = ((posY / rect.height) * 200) - 100,
        x = (percentX * 0.05).toFixed(2),
        y = (percentY * 0.05).toFixed(2);

    pos.x = x * -1;
    pos.y = y * -1;

    if (needAnimFrame) {
      needAnimFrame = false;         



We have also added a subtle parallax effect to the example, via the excellent basicScroll plugin. The scroll position is monitored by the plugin and updates the --scroll variable by a margin of 25px on either side. This then animates the img element within its parent via object-position and the calc() function:

.workshop-photo {
  &__holder {
    height: 200px;
    overflow: hidden;
  &__image {
    object-position: 50% 50%;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 50% calc(50% + calc(var(--scroll) * 1px));
    will-change: object-position;

const initParallax = () => {

  let $parallax = document.querySelector('[data-parallax]');

    elem: $parallax,
    from: 'top-bottom',
    to: 'bottom-top',
    direct: true,
    props: {
      '--scroll': {
        from: '-25',
        to: '25'


Please note - certain parts of the above code blocks were omitted for brevity - and, due to the modern techniques used, this example will probably only work in sensible browsers.. i.e not IE!


That said, there are some disadvantages. Browser support is good but not total - standing at 92.48% at the time of writing. IE is the main stickler, but with IE11 almost six years old now and functions and polyfills available this isn’t a big disadvantage. You also can’t use them in certain scenarios, such as in media queries or particular pseudo selectors, but a preprocessor can help here if you use one.

CSS variables are an incredibly useful addition to a Front End or UI Developer’s toolkit; allowing for greater code flexibility and clarity, less repetition and simpler, reactive and more robust interface animations. Any disadvantage can be negated with a polyfill so there really isn’t an excuse not to leverage their power.