CSS-driven scroll effects

ScrollVar

Add JS once. Control everything through CSS.

One CSS variable. Unlimited effects. All logic lives in CSS.

Scroll to see 50 effects

Effect 01 — Parallax

Depth from one variable

Three layers, three --speed values. Mobile override in pure @media.

.layer-1 { --speed: 0.05; }
.layer-2 { --speed: 0.15; }
.layer-3 { --speed: 0.30; }

.layer {
  transform: translateY(calc(var(--scroll-offset) * var(--speed) * 1px));
  transition: transform 0.12s ease-out;
}

Effect 02 — Fade

Cards fade at the edges

abs() makes the effect symmetrical — fades both on enter and exit.

.card {
  opacity: clamp(0, calc(1 - abs(var(--scroll-offset)) / 380), 1);
  transform: translateY(calc(var(--scroll-offset) * 0.04px));
  transition: opacity 0.15s ease-out, transform 0.15s ease-out;
}

CSS-first

All effect logic lives in CSS.

~700 bytes

Zero dependencies, single file.

@media ready

Override any variable in media queries.

Effect 03 — Hue rotate

Any CSS property

Not just transforms. filter, background, clip-path — anything that accepts a number.

.gradient {
  filter: hue-rotate(
    calc(var(--scroll-offset) / 2 * 1deg)
  );
  transition: filter 0.15s ease-out;
}

Effect 04 — Scale

Scale + parallax combined

One variable, two effects. Odd items float up, even float down.

.item {
  scale: clamp(0.4, calc(1 - abs(var(--scroll-offset)) / 500), 1.15);
  transition: scale 0.15s ease-out;
}

Effect 05 — 3D Rotate

Natural 3D tilt

Scroll offset drives both rotateY and rotateX. Parent needs perspective.

.card {
  transform:
    rotateY(calc(var(--scroll-offset) / 8 * 1deg))
    rotateX(calc(var(--scroll-offset) / -12 * 1deg));
  transition: transform 0.15s ease-out;
}
.scene { perspective: 900px; }

Effect 06 — Clip-path reveal

Bars reveal at center

clip-path: inset() driven by abs(--scroll-offset) — fully visible only at viewport center.

.bar {
  clip-path: inset(0
    calc(clamp(0%, (abs(var(--scroll-offset)) - 60) / 3 * 1%, 100%))
    0 0 round 12px);
  transition: clip-path 0.15s ease-out;
}

Parallax
Fade + Blur
Hue rotate + Color temp
Scale · 3D · Clip · Progress
0%
scroll progress

Effect 07 — Progress ring

SVG circle fill

stroke-dashoffset on an SVG circle — fills to 100% when element is centered. Works with CSS or driven via JS update.

/* 2π × r = 691 for r=110 */
.ring {
  stroke-dasharray: 691;
  stroke-dashoffset: clamp(0px,
    calc(691px * (1 - clamp(0,
      1 - abs(var(--scroll-offset)) / 320,
    1))), 691px);
  transition: stroke-dashoffset 0.2s ease-out;
}

Effect 08 — Blur focus

Sharpens at center

Items are blurry when away from viewport center, and come into focus as they approach it.

.item {
  filter: blur(clamp(0px, abs(var(--scroll-offset)) / 40 * 1px, 12px));
  opacity: clamp(0.15, calc(1 - abs(var(--scroll-offset)) / 350), 1);
  transition: filter 0.2s ease-out, opacity 0.2s ease-out;
}

Sharp

Focus

Clear

Crisp

Effect 09 — Letter spacing

Typography expands

Letters spread apart as the element scrolls away from center. Combine with opacity for a cinematic feel.

SCROLL
DRIVEN
CSS

Effect 10 — Border radius morph

Square morphs to circle

border-radius driven by abs(--scroll-offset) — perfectly square at center, circle at edges.

.item {
  border-radius: clamp(12px, abs(var(--scroll-offset)) / 5 * 1px, 50%);
  transition: border-radius 0.2s ease-out;
}

Effect 11 — Skew

Alternating skew

Odd bands skew right, even skew left — created with :nth-child(even) negating the same variable.

.band              { transform: skewX(calc(var(--scroll-offset) /  20 * 1deg)); }
.band:nth-child(even) { transform: skewX(calc(var(--scroll-offset) / -20 * 1deg)); }

Skew right →
← Skew left
Skew right →

Effect 12 — Box shadow depth

Cards float to the surface

Shadow grows as the element reaches viewport center — like a card physically rising off the page.

.card {
  box-shadow: 0
    calc((1 - abs(var(--scroll-offset)) / 280) * 40px)
    calc((1 - abs(var(--scroll-offset)) / 280) * 80px)
    rgba(108,92,231, calc((1 - abs(var(--scroll-offset)) / 280) * 0.5));
  transition: box-shadow 0.2s ease-out, transform 0.2s ease-out;
}

Floating

Deep shadow at center

Rising

Elevates on approach

Hovering

Falls back at edges

🌡️

Effect 13 — Color temperature

Cool → warm on scroll

HSL hue shifts from 220° (cool blue) through 120° (green) to 30° (warm orange) as the element scrolls.

.swatch {
  background: hsl(
    calc(220deg - clamp(-100deg,
      var(--scroll-offset) / 4 * 1deg,
      100deg)),
    80%, 55%
  );
  transition: background 0.2s ease-out;
}

Effect 14 — Progress bars

Fill on center

Bars fill to 100% exactly when the element center hits the viewport center — great for skill bars or step indicators.

.fill {
  width: clamp(0%,
    calc((1 - abs(var(--scroll-offset)) / 320) * 100%),
    100%);
  transition: width 0.2s ease-out;
}

Parallax
Fade & Blur
Scale & Morph
3D · Skew · Shadow
40px
grid cell size

Effect 15 — Background zoom

Grid cell size on scroll

background-size changes with scroll — the grid zooms in or out. Works for any repeating pattern: dots, lines, checkerboard.

.grid {
  background-image:
    repeating-linear-gradient(0deg, ...),
    repeating-linear-gradient(90deg, ...);
  background-size:
    calc(40px + clamp(0px, abs(var(--scroll-offset)) / 8 * 1px, 80px))
    calc(40px + clamp(0px, abs(var(--scroll-offset)) / 8 * 1px, 80px));
  transition: background-size 0.2s ease-out;
}

Effect 16 — translateX

Horizontal drift

Elements drift left or right. Odd cards go right, even go left — one variable, opposite directions via sign inversion.

Effect 17 — rotate (Z axis)

Spin on scroll

Each dial rotates at a different speed. Scroll direction maps directly to clockwise / counter-clockwise rotation.

Back
Mid
Front

Effect 18 — translateZ + perspective

Depth zoom

Cards at different Z depths — the closer the card, the faster it approaches. Pure CSS perspective.

.tz-scene { perspective: 800px; }
.back  { transform: translateZ(calc(var(--scroll-offset) * -0.2px)); }
.mid   { transform: translateZ(calc(var(--scroll-offset) * -0.5px)); }
.front { transform: translateZ(calc(var(--scroll-offset) * -0.9px)); }

Effect 19 — skewY

Vertical shear

skewY creates a paper-fold illusion. Combined with opacity it looks like pages turning.

Shear →
← Shear
Shear →

Effects 20–24 — Five filters in one section

brightness · contrast · saturate · grayscale · sepia

All five driven by the same variable. Each filter responds to abs(--scroll-offset) — maximum effect at the edges, neutral at center.

brightness
contrast
saturate
grayscale
sepia

Effects 25–26 — invert · drop-shadow

Negative & filter shadow

invert() flips colors — 0% at center, 100% at edges. drop-shadow() blur radius grows on approach.

invert
drop-shadow

Effects 27–28 — text color · border color

Color driven by scroll

HSL hue shifts on both color and border-color. The hue angle is calculated directly from --scroll-offset.

Scroll
drives
color

Effect 29 — text-shadow

Glowing text

Blur radius of text-shadow shrinks to 0 at center — the text sharpens and glows as it reaches focus.

FOCUS
CENTER
GLOW

Effect 30 — font-size

Breathing typography

Font size peaks at viewport center and shrinks toward the edges — like a word physically stepping into a spotlight.

BIG AT CENTER

Effect 31 — word-spacing

Words breathe apart

word-spacing at its natural value at center, expands toward the edges — a typographic stretch effect.

Design driven by CSS

One variable rules all

Scroll to transform

Effect 32 — line-height

Lines open and close

line-height is tight at the edges, relaxed at center. Feels like the text is breathing.

One CSS variable.
Unlimited effects.
No config in JS.
All logic in CSS.

Effect 33 — clip-path circle()

Circle reveal

Circular mask grows from 0 to 100% as element approaches center. Different from inset() — grows radially from the middle.

.box {
  clip-path: circle(
    clamp(0%, calc(
      (1 - abs(var(--scroll-offset)) / 320) * 80%
    ), 80%)
  );
  transition: clip-path 0.2s ease-out;
}

Effect 34 — clip-path polygon

Square → star morph

Polygon vertices are driven by --scroll-offset. At center: square. At edges: star with inward points.

.star {
  clip-path: polygon(
    50% calc(clamp(0%, abs(var(--scroll-offset))/5*1%, 50%)),
    100% 50%, 50% 100%, 0% 50%
  );
  transition: clip-path 0.2s ease-out;
}

Effect 35 — outline-offset

Pulsing outline ring

outline-offset expands away from the element at the edges and collapses to zero at center — a radar-ping feel.

Effect 36 — border-width

Border grows at center

border-width peaks when element is at viewport center. The thickest border = perfect position indicator.

01
02
03
04

Effect 37 — stroke-width (SVG)

SVG lines breathe

SVG stroke-width driven by CSS variable — concentric rings, each at different breath rate.

circle {
  stroke-width: clamp(1px,
    calc((1 - abs(var(--scroll-offset)) / 300) * 20px),
  20px);
  transition: stroke-width 0.2s ease-out;
}

Effect 38 — SVG fill + opacity

SVG shapes shift color

CSS custom properties work on SVG fill and opacity just like HTML elements.

Effect 39 — background-position

Background parallax

The background image moves at a slower rate than the element — classic parallax without any extra DOM elements.

.el {
  background-position:
    50%
    calc(50% + var(--scroll-offset) * 0.3px);
  transition: background-position 0.2s ease-out;
}

Effect 40 — stagger (nth-child delay)

Cascading entrance

One ScrollVar instance, one variable. The stagger is pure CSS — transition-delay via :nth-child.

.item:nth-child(1) { transition-delay: 0ms; }
.item:nth-child(2) { transition-delay: 60ms; }
.item:nth-child(3) { transition-delay: 120ms; }

01
02
03
04
05
06
07
08
09

Effect 41 — backdrop-filter blur

Frosted glass

The glass panel's blur radius responds to scroll — clear at center, fully frosted at the edges.

.glass {
  backdrop-filter: blur(
    clamp(0px,
      abs(var(--scroll-offset)) / 15 * 1px,
    24px)
  );
  transition: backdrop-filter 0.2s ease-out;
}

Effect 42 — multi-transform stack

translate + rotate + scale

All three transforms applied simultaneously from one variable. The element orbits into position as it reaches center.

1
2
3

Effect 43 — perspective squish

Near & far

Cards shrink to nothing at edges and spring to full size at center — the scaleX/scaleY split creates a squash effect.

Far ↑
Near
Far ↓

Effect 44 — outline-color

Chromatic focus ring

outline-color cycles through the spectrum as the element scrolls — like a chromatic aberration ring around focused UI elements.

Effect 45 — scaleX / scaleY split

Squash & stretch

When scaleX grows, scaleY shrinks proportionally — the cartoon squash-and-stretch illusion. Preserves area while deforming shape.

Effect 46 — conic-gradient rotation

Spinning gradient

hue-rotate on a conic gradient — the color wheel spins as you scroll. The starting angle maps to --scroll-offset.

.wheel {
  background: conic-gradient(
    from calc(var(--scroll-offset) * 0.5deg),
    #6c5ce7, #fd79a8, #fdcb6e,
    #00b894, #6c5ce7
  );
  transition: background 0.15s ease-out;
}

Effect 47 — text-indent

Cinematic text entrance

text-indent slides text in from the left. Combined with opacity it looks like a title card appearing on screen.

SCROLL
DRIVEN
MOTION

Effect 48 — translateY stagger wave

Wave entrance

Each item has the same variable but a different transition-delay — the wave cascades across the row purely in CSS.

A
B
C
D
E
F
G

Effect 49 — composite (6 effects in 1)

Everything at once

One element. One variable. Six simultaneous CSS effects: translate, rotate, scale, blur, hue-rotate, opacity.

.card {
  transform:
    translateY(calc(var(--scroll-offset) * 0.3px))
    rotate(calc(var(--scroll-offset) / 10 * 1deg))
    scale(clamp(0.3, calc(1 - abs(var(--scroll-offset))/400), 1.1));
  filter:
    blur(clamp(0px, abs(var(--scroll-offset))/30*1px, 8px))
    hue-rotate(calc(var(--scroll-offset) * 0.5deg));
  opacity: clamp(0.1, calc(1 - abs(var(--scroll-offset))/400), 1);
}

Effect 50 — all-in-one showcase strip

50 effects. One variable.

Each tile uses a different CSS property. All driven by a single new ScrollVar('.showcase-tile').

translateY
translateX
rotateZ
rotateX
scale
skewX
opacity
blur
hue
bright

How it works

One number. Endless CSS.

ScrollVar writes (elementCenter − viewportCenter) in pixels to a CSS variable on every scroll frame.

JS — one line

new ScrollVar('.hero');

CSS — your logic

.hero {
  --speed: 0.15;
  transform: translateY(
    calc(var(--scroll-offset)
      * var(--speed) * 1px)
  );
}

Variable value

/* negative → element below center */
/* zero     → element at center    */
/* positive → element above center */

elementCenter − viewportCenter

Per-element name

<section
  data-scroll-var="--hero-offset"
></section>

new ScrollVar('[data-scroll-var]');

Get started

Two ways to use


1
Local file
copy scrollvar.js to your project
2
ES module
import ScrollVar from './scrollvar.js'