Post

CSS Variables: Beyond Theming

CSS Variables: Beyond Theming

CSS Variables aren’t just for storing colors and spacing values. They can create smooth, performant interactions by letting JavaScript update values while CSS handles the rendering.

Example 1: Scroll Progress Bar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html>
<head>
<style>
:root {
  --scroll-progress: 0;
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 4px;
  background: linear-gradient(to right, blue, purple);
  width: calc(var(--scroll-progress) * 1%);
  transition: width 0.1s;
}

.content {
  height: 300vh;
  padding: 20px;
}
</style>
</head>
<body>

<div class="progress-bar"></div>
<div class="content">
  <h1>Scroll Down</h1>
  <p>Content...</p>
</div>

<script>
window.addEventListener('scroll', () => {
  const scrollTop = window.scrollY;
  const docHeight = document.documentElement.scrollHeight - window.innerHeight;
  const progress = (scrollTop / docHeight) * 100;
  
  document.documentElement.style.setProperty('--scroll-progress', progress);
});
</script>

</body>
</html>

Example 2: Mouse Follower

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html>
<head>
<style>
.box {
  --x: 0;
  --y: 0;
  width: 100px;
  height: 100px;
  background: blue;
  transform: translate(
    calc(var(--x) * 1px), 
    calc(var(--y) * 1px)
  );
}
</style>
</head>
<body>

<div class="box" id="box"></div>

<script>
const box = document.getElementById('box');

document.addEventListener('mousemove', (e) => {
  box.style.setProperty('--x', e.clientX);
  box.style.setProperty('--y', e.clientY);
});
</script>

</body>
</html>

But Why Use CSS Variables?

You might wonder: why not just update the style directly?

1
2
3
// Why not just do this?
element.style.left = x + 'px';
element.style.top = y + 'px';

The answer lies in how browsers render web pages.

How Browsers Render

The browser’s rendering pipeline has three main stages:

1
2
3
Layout → Paint → Composite
  ↓       ↓        ↓
 SLOW   SLOW     FAST

Layout: Calculate position and size of every element
Paint: Draw pixels (colors, text, borders)
Composite: Combine layers using GPU

The Key Difference

Direct DOM manipulation:

1
element.style.left = '100px';  

Modifying properties like left, top, width, height triggers Layout (reflow), which forces the browser to recalculate positions for the entire page, then repaint everything.

CSS Variables + Transform:

1
element.style.setProperty('--x', 100);
1
transform: translateX(calc(var(--x) * 1px));

The transform property skips Layout and Paint entirely, going straight to the Composite stage where the GPU handles it.

Visual Comparison

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html>
<head>
<style>
.box {
  width: 50px;
  height: 50px;
  background: blue;
  position: absolute;
}

.box-transform {
  --x: 0;
  --y: 0;
  transform: translate(
    calc(var(--x) * 1px),
    calc(var(--y) * 1px)
  );
}
</style>
</head>
<body>

<div class="box" id="box1"></div>
<div class="box box-transform" id="box2"></div>

<div style="margin-top: 100px;">
  <p>Box 1: Direct DOM (left/top) - slower</p>
  <p>Box 2: CSS Variables (transform) - faster</p>
</div>

<script>
const box1 = document.getElementById('box1');
const box2 = document.getElementById('box2');

let x = 0;

// Method 1: Direct DOM
function animateSlow() {
  x += 1;
  box1.style.left = x + 'px';
  box1.style.top = (Math.sin(x / 50) * 100 + 200) + 'px';
  
  if (x < 500) requestAnimationFrame(animateSlow);
}

// Method 2: CSS Variables
function animateFast() {
  x += 1;
  box2.style.setProperty('--x', x);
  box2.style.setProperty('--y', Math.sin(x / 50) * 100 + 200);
  
  if (x < 500) requestAnimationFrame(animateFast);
}

// Run both
setTimeout(() => animateSlow(), 1000);
setTimeout(() => animateFast(), 2000);
</script>

</body>
</html>

Watch the two blue boxes move—Box 1 (direct DOM) stutters noticeably compared to Box 2 (CSS Variables).


This post is licensed under CC BY 4.0 by the author.