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).