GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Horizontal Scroll Animation CSS: 9 Inertia Scenes

Horizontal scroll animation css is more than turning wheel input sideways — scene 01 glides it on inertia, 06 projects cards onto a cylinder, and 08 bends edge

Roll a Wheel Over the Nine Sideways Tracks Before the Code

Each card in the grid below jumps to its own section, and inside every demo a wheel turn sends the track sliding sideways at once. The order follows how many layers sit between your input and the picture: 01 to 03 are three stages of input handling (inertia, segment mapping, fling snap), 04 to 07 are four ways to make a sideways move read as depth (lighting, parallax, cylinder, cord), and 08 and 09 are two finishes where pixels and letters bend.

Auto-plays · click a tile to jump to its section · all nine in one zip

Horizontal Scroll Animation CSS: 9 Sideways Screens From Wheel Inertia, Cylinder Projection and a Fisheye Lens

To be upfront about the title: every one of the nine scenes is drawn on a <canvas> by JavaScript that recalculates each frame, and the only CSS animation on the page is a transition that changes a button's background on hover. Turning the vertical value of a wheel into sideways travel and giving it inertia is beyond what CSS properties can do, so the scripts read the wheel event directly. CSS handles the frame around the stage: width, aspect ratio, corners and the shape of the status pill.

The nine files share one skeleton. Each frame adds its rAF time delta, capped at 100ms, to an accumulator, runs the fixed 16ms step (FIXED_DT) up to six times and then draws once, so a tab that was hidden and comes back does not jump ahead. No random numbers are involved: the ridges of 05 come from an integer hash, the auto demos run on fixed schedules, and libraries, CDNs and WebGL appear zero times across the nine files.

Handling horizontal scroll css mouse wheel input is the same two-step routine in every scene. First wheelPx puts the raw value on one scale: line-based devices (deltaMode 1) are multiplied by 16, page-based ones (2) by 480, and when |deltaX| > |deltaY| the horizontal value wins, so a trackpad's sideways swipe and a mouse wheel's vertical roll arrive on the same axis and take the same path. Then canMove(d) asks whether the track can still travel that way, and preventDefault runs only when it can; a probe found that wheel events toward an end pass through while those in a movable direction are blocked, in all nine scenes. That is how the page's own vertical scroll carries on once a track reaches its end.

The first time a person touches the wheel, pointer or keys, wake() stops the auto demo and switches the status pill's aria-live from off to polite, so a screen reader stays quiet while the demo plays by itself. The canvas also carries touch-action: pan-y, which hands a sideways finger drag to the scene and leaves a vertical swipe to the browser.

How Far Does a Horizontal Scroll CSS Example Go Without a Library?

Further than you might expect, because inertia, snapping, perspective projection and the pixel remap all run on a few lines of multiplication and addition. The core math of 01 is one geometric series, 03 is a two-term spring, 06 is one division, and the ridges of 05 come from four layers of grid hashing. Picking by use: product rows and recommendation strips start from 01, product landing pages and timelines from 02, mobile card carousels from 03, step tours and brand stories from 04, travel heroes from 05, album and portfolio covers from 06, photo lines from 07, icon docks and chip rows from 08, and slogan bands from 09.

A horizontal scroll animation codepen is handy for a quick look, but here each scene runs in place with its code right under it. The snippets in the nine sections below keep the exact lines and indentation of each scene's index.html script, so the same function names lead you back to the full source once the zip is open.

01A Card Track Where Vertical Wheel Input Becomes Sideways Motion and Friction Slows It After Input Stops

One vertical wheel notch (100) times WHEEL_GAIN 0.12 becomes a starting speed of 12px per step, and FRICTION 0.92 multiplies it on every step after that, so the track glides 149.4px over 1056ms before it stops. Past either end the input shrinks to d/(1+o/RUBBER_PX) with the distance already overshot, holding like a rubber band, while keys use speedFor to solve backward for the start speed that lands exactly on the target. The render measured 41.4% covered area, 109.7 intensity and 16/23 moved frames, the gap coming from a stretch where the track rests.

A Card Track Where Vertical Wheel Input Becomes Sideways Motion and Friction Slows It After Input Stops
Open in a new tab

01 · A Card Track Where Vertical Wheel Input Becomes Sideways Motion and Friction Slows It After Input Stops — Measured area 41.419% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

FRICTIONWHEEL_GAINEDGE_KRUBBER_PX
    // Wheel travel per event in px — line-based (deltaMode 1) and page-based (deltaMode 2) devices are put on the same scale.
    // Use whichever axis is larger: a trackpad's sideways swipe and a mouse wheel's vertical roll come in the same direction
    function wheelPx(ev) {
        const unit = ev.deltaMode === 1 ? 16 : (ev.deltaMode === 2 ? W : 1);
        const d = Math.abs(ev.deltaX) > Math.abs(ev.deltaY) ? ev.deltaX : ev.deltaY;
        return d * unit;
    }
    // Input pushing past an end shrinks with the distance already out — it holds like a rubber band
    function rubber(d) {
        const out = (x <= 0 && d < 0) || (x >= MAX_X && d > 0);
        return out ? d / (1 + Math.abs(overshoot(x)) / RUBBER_PX) : d;
    }
    function push(d) {
        v += rubber(d) * WHEEL_GAIN;
        moving = true;
        say(TXT_GLIDE);
    }
    // How much farther it goes if left at this speed — FRICTION multiplies each step, so the geometric series v·f/(1−f)
    function glideDistance(v0, f) { return v0 * f / (1 - f); }
    // In reverse: the start speed needed to stop after exactly distance
    function speedFor(distance, f) { return distance * (1 - f) / f; }
    function canMove(d) { return d > 0 ? x < MAX_X - 0.5 : x > 0.5; }

02A Pinned Panel Scene Where the Section Stays Fixed and Vertical Progress Splits Into Sideways Moves per Panel

Progress from 0 to 1 is split into three segments between the four panels, and inside each segment the first and last 18% (HOLD) stay put while the middle 64% crosses on smoothstep, so the view steps over one panel at a time. A 100px wheel move adds 0.09 of progress and the view follows through PIN_LERP 0.12, so it trails the input by about 128ms. Measured: 50.4% covered area, 94.1 intensity, 23/23 moved frames.

A Pinned Panel Scene Where the Section Stays Fixed and Vertical Progress Splits Into Sideways Moves per Panel
Open in a new tab

02 · A Pinned Panel Scene Where the Section Stays Fixed and Vertical Progress Splits Into Sideways Moves per Panel — Measured area 50.447% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PANEL_NHOLDP_GAINPIN_LERP
    // Progress p (0–1) → panel position (0–PANEL_N−1). Each segment holds for HOLD at both ends and crosses the middle with smoothstep
    function panelPos(p) {
        const segs = PANEL_N - 1;
        const s = clamp(p, 0, 1) * segs;
        const k = Math.min(Math.floor(s), segs - 1);
        const u = s - k;
        return k + smooth((u - HOLD) / (1 - 2 * HOLD));
    }
    function canMove(d) { return d > 0 ? target < 1 : target > 0; }

03A Drag Snap That Predicts the Landing Point From Release Speed and Springs Into the Nearest Slot

On release, predictSlot picks the landing slot first with round((x + v·0.2)/232), and a spring a = 120·(target−x) − 14·v pulls the track toward it, with a damping ratio of 0.639 that gives 5.5% overshoot and settles in 912ms. A wheel leaves no velocity behind, so 140ms after the last roll the track snaps to the slot nearest its current spot, and only a drag produces a fling. The render gave 46.5% covered area, 83.3 intensity and 21/23 moved frames.

A Drag Snap That Predicts the Landing Point From Release Speed and Springs Into the Nearest Slot
Open in a new tab

03 · A Drag Snap That Predicts the Landing Point From Release Speed and Springs Into the Nearest Slot — Measured area 46.493% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

TAUSNAP_KSNAP_CPITCH
    // Distance dragged past an end is shown shortened — a rubber band
    function rubberClamp(p) {
        if (p < 0) {
            return p / (1 - p / RUBBER_PX);
        }
        if (p > MAX_X) {
            const o = p - MAX_X;
            return MAX_X + o / (1 + o / RUBBER_PX);
        }
        return p;
    }
    // From the release speed, look τ seconds ahead and pick the nearest slot
    function predictSlot(x0, v0) {
        return clamp(Math.round((x0 + v0 * TAU) / PITCH), 0, CARD_N - 1);
    }
    // One spring-damper step — acceleration = K·(target−position) − C·velocity (semi-implicit Euler)
    function springStep(dt) {
        const a = SNAP_K * (target - x) - SNAP_C * v;
        v += a * dt;
        x += v * dt;
    }
    function canMove(d) { return d > 0 ? x < MAX_X - 0.5 : x > 0.5; }

04Folding-Screen Panels That Fold and Unfold With Sideways Progress, Each Face Lit by Its Angle to the Light

The fold angle θ of all six panels comes from a single progress value, and each corner is projected through persp(z) = 420/(420+z) so the faces become trapezoids. Brightness is the dot product of each face normal with the light direction (−0.6, −0.3, −0.75): fully folded, even faces sit at 0.3 and odd faces at 0.961, and fully open, all six land on the same 0.686. Covered area came to 41.9%, intensity 59.5 and moved frames 21/23.

Folding-Screen Panels That Fold and Unfold With Sideways Progress, Each Face Lit by Its Angle to the Light
Open in a new tab

04 · Folding-Screen Panels That Fold and Unfold With Sideways Progress, Each Face Lit by Its Angle to the Light — Measured area 41.942% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

MAX_FOLDFOCALYAWAMBIENT
    // Dot product of face i's normal with the light → brightness. Even faces fold right and inward, odd faces fold right and forward
    function panelLight(i, th) {
        const n = yawRot((i % 2 === 0 ? 1 : -1) * Math.sin(th), -Math.cos(th));
        const dot = n[0] * LIGHT[0] + n[1] * LIGHT[2];
        return AMBIENT + DIFFUSE * Math.max(0, dot);
    }
    // Point on face i (u: 0 left to 1 right) → sideways position and depth before rotation
    function panelLocal(i, u, th) {
        const run = PANEL_W * Math.cos(th);
        const rise = PANEL_W * Math.sin(th);
        const total = PANEL_N * run;
        const X = W / 2 - total / 2 + (i + u) * run;
        const Z = i % 2 === 0 ? u * rise : (1 - u) * rise;
        return yawRot(X - W / 2, Z);
    }

05A Sideways Parallax Landscape Where Noise-Drawn Ridges, Hills and Tree Layers Drift at Speeds Set by Depth

Ridge height is fBm built from value noise over an integer grid hash (hash1), four octaves stacked and normalized to 0–1, so for any real scroll value the curve has no seam, with the largest jump between 0.25px steps at 0.337px. The five layers divide the travel by depths [8, 5, 3.2, 2, 1.2], so 1000px of scroll moves them anywhere from 125px to 833.3px, a 6.67x spread. It measured 37.1% covered area, 39.3 intensity and 23/23 moved frames.

A Sideways Parallax Landscape Where Noise-Drawn Ridges, Hills and Tree Layers Drift at Speeds Set by Depth
Open in a new tab

05 · A Sideways Parallax Landscape Where Noise-Drawn Ridges, Hills and Tree Layers Drift at Speeds Set by Depth — Measured area 37.122% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SEEDOCTAVESDEPTHSFREQS
    // Value of one integer grid cell — the same seed gives the same number every time (integer-mixing hash)
    function hash1(n, seed) {
        let h = (Math.imul(n | 0, 374761393) + Math.imul(seed, 668265263)) | 0;
        h = Math.imul(h ^ (h >>> 13), 1274126177);
        h ^= h >>> 16;
        return (h >>> 0) / 4294967296;
    }
    // 1D value noise — joins two grid values with smoothstep. The grid runs on without end, so there is no seam
    function vnoise(x, seed) {
        const i = Math.floor(x);
        const f = x - i;
        const u = f * f * (3 - 2 * f);
        return lerp(hash1(i, seed), hash1(i + 1, seed), u);
    }
    // fBm — stacks OCTAVES layers, doubling frequency and halving strength each time, then scales to 0–1
    function fbm(x, seed) {
        let sum = 0;
        let amp = 0.5;
        let freq = 1;
        let norm = 0;
        for (let o = 0; o < OCTAVES; o += 1) {
            sum += amp * vnoise(x * freq, seed + o * 101);
            norm += amp;
            amp *= 0.5;
            freq *= 2;
        }
        return sum / norm;

06A Cylinder Gallery Where Cards Turn Around a Drum and Perspective Sets Their Size, Brightness and Order

Each card has an angle on the drum, and s = 270/(520 − 250·cosφ) turns it into a screen position and scale, so the front card comes out at 1.000, a side card at 0.519 and a back card at 0.351 with no separate sizing code. facing treats a card as turned toward the camera while cosφ stays above 250/520, which is ±61.3°, and draws its back past that, while brightness blends from 0.35 to 1 by (cosφ+1)/2, so only the card directly behind sits at 0.35 and a card at ±61.3° comes out near 0.83; the draw order is re-sorted every frame by ascending cosφ, farthest first. The render recorded 28.8% covered area, 87.3 intensity and 17/23 moved frames.

A Cylinder Gallery Where Cards Turn Around a Drum and Perspective Sets Their Size, Brightness and Order
Open in a new tab

06 · A Cylinder Gallery Where Cards Turn Around a Drum and Perspective Sets Their Size, Brightness and Order — Measured area 28.848% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

RADIUSCAM_DSTEP_ANGBACK_DIM
    // Point at angle φ on the cylinder → screen x and perspective scale. The camera sits CAM_D from the axis and the front card has scale 1
    function project(phi) {
        const X = RADIUS * Math.sin(phi);
        const Z = CAM_D - RADIUS * Math.cos(phi);
        const s = (CAM_D - RADIUS) / Z;
        return [W / 2 + X * s, s];
    }
    // Screen y for height h (card center 0, down +) — the camera is LIFT above, so farther cards rise toward the horizon (HORIZON)
    function screenY(h, s) { return HORIZON + (h + LIFT) * s; }
    // Is it facing the camera — dot product of the outward normal and the camera direction = CAM_D·cosφ − R
    function facing(phi) { return CAM_D * Math.cos(phi) - RADIUS; }
    function canMove(d) { return d > 0 ? rot < MAX_ROT - 0.001 : rot > 0.001; }

07A Row of Cards Hung on a Cord That Get Pulled Sideways After the Lead Card, Swing and Settle

The 49 nodes of the cord use Verlet integration with no velocity array, taking the gap between current and previous positions as velocity, and a constraint that pulls any neighbor pair stretched past REST back to that length, leaving shorter pairs alone, runs 10 times per step. At start, rest(240) runs 240 steps ahead of time, so the scene opens on a stable sag of up to 57.9px, and each card turns by the atan2 slope at its own node. It measured 50.2% covered area and 23/23 moved frames, and its 119.4 intensity is the highest of the nine.

A Row of Cards Hung on a Cord That Get Pulled Sideways After the Lead Card, Swing and Settle
Open in a new tab

07 · A Row of Cards Hung on a Cord That Get Pulled Sideways After the Lead Card, Swing and Settle — Measured area 50.2% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SLACKGRAVITERCARD_WEIGHT
    function relax() {
        for (let it = 0; it < ITER; it += 1) {
            for (let i = 0; i < NODE_N; i += 1) {
                if (isPost(i)) {
                    px[i] = postX(i);
                    py[i] = CORD_Y;
                }
            }
            for (let i = 0; i < NODE_N - 1; i += 1) {
                const dx = px[i + 1] - px[i];
                const dy = py[i + 1] - py[i];
                const d = Math.sqrt(dx * dx + dy * dy) || 1;
                if (d <= REST) {
                    continue;
                }
                const diff = (d - REST) / d;
                const a = isPost(i) ? 0 : (isPost(i + 1) ? 1 : 0.5);
                const b = isPost(i + 1) ? 0 : (isPost(i) ? 1 : 0.5);
                px[i] += dx * diff * a;
                py[i] += dy * diff * a;
                px[i + 1] -= dx * diff * b;
                py[i + 1] -= dy * diff * b;
            }
        }
    }
    // Cord slope at the clip (radians) — the angle of the line joining the neighbors on each side
    function slopeAt(i) {
        const a = Math.max(0, i - 1);
        const b = Math.min(NODE_N - 1, i + 1);
        return Math.atan2(py[b] - py[a], px[b] - px[a]);
    }

08An Edge Lens Where Cards Near Both Ends of the Track Bend and Shrink Like a Fisheye and Flatten Out in the Middle

The source icon row (1730px) is drawn once at start and read with getImageData, with no reads after that, and each redraw, which happens only when a dirty flag says the lens or position changed, walks the 144,000 screen pixels, using the lens curve in lensSample to find each source position backward and copy its color. Vertically each column is divided by the lensScale magnification with a BEND·u² sag added to fake a convex surface, giving an edge scale of 0.488 against a flat 156px band in the middle. Measured: 42.5% covered area, 72.6 intensity, 22/23 moved frames.

An Edge Lens Where Cards Near Both Ends of the Track Bend and Shrink Like a Fisheye and Flatten Out in the Middle
Open in a new tab

08 · An Edge Lens Where Cards Near Both Ends of the Track Bend and Shrink Like a Fisheye and Flatten Out in the Middle — Measured area 42.474% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

LENS_KLENS_PBENDPITCH
    // Screen x position u (−1 to 1) → source sample position s. s = u·(1 + K·|u|^P) — the closer to the edge, the more source it squeezes in
    function lensSample(u, k) { return u * (1 + k * Math.pow(Math.abs(u), LENS_P)); }
    // Magnification du/ds at that point — 1 in the middle, 1/(1 + K·(P+1)) at the edge
    function lensScale(u, k) { return 1 / (1 + k * (LENS_P + 1) * Math.pow(Math.abs(u), LENS_P)); }
    // Half of the source width that half the screen (240px) holds at lens strength k
    function halfSpan(k) { return (W / 2) * (1 + k); }
        for (let x = 0; x < W; x += 1) {
            const u = (x + 0.5 - W / 2) / (W / 2);
            const sx = Math.round(center + lensSample(u, kNow) * (W / 2));
            const sc = lensScale(u, kNow);
            const sag = BEND * u * u * (kNow / LENS_K);
            for (let y = 0; y < H; y += 1) {
                const sy = Math.round(CY + (y - CY - sag) / sc);
                const o = (y * W + x) * 4;
                if (sx < 0 || sx >= SRC_W || sy < 0 || sy >= H) {
                    dst[o] = bgR;
                    dst[o + 1] = bgG;
                    dst[o + 2] = bgB;
                    dst[o + 3] = 255;
                    continue;
                }
                const i = (sy * SRC_W + sx) * 4;
                dst[o] = src[i];
                dst[o + 1] = src[i + 1];
                dst[o + 2] = src[i + 2];
                dst[o + 3] = 255;
            }
        }

09A Text Ribbon That Flows Sideways Along a Sine Wave, Each Letter Turned to the Slope, With Taller Waves as Speed Builds

Letters are placed by walking the sine curve by arc length, so advancing ds moves ds/√(1+slope²) across the screen, and each letter turns by the atan of the slope at its spot. Wave height is a low-pass follower of speed, chasing the target clamp(10 + 0.09|speed|, 10, 58) with a 320ms lag, and it saturates at 533px/s. The scene came out at 30.3% covered area, 84.8 intensity and 23/23 moved frames.

A Text Ribbon That Flows Sideways Along a Sine Wave, Each Letter Turned to the Slope, With Taller Waves as Speed Builds
Open in a new tab

09 · A Text Ribbon That Flows Sideways Along a Sine Wave, Each Letter Turned to the Slope, With Taller Waves as Speed Builds — Measured area 30.333% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WAVE_KAMP_LERPAMP_GAINSPEED_FRICTION
    // Wave curve and its derivative (slope) — each letter turns by atan(slope)
    function waveY(x) { return CY + amp * Math.sin(WAVE_K * x + phase); }
    function waveSlope(x) { return amp * WAVE_K * Math.cos(WAVE_K * x + phase); }
    // Flow speed → target wave height. The actual height follows it slowly through a low-pass filter (AMP_LERP)
    function ampTarget(speed) { return clamp(AMP_MIN + AMP_GAIN * Math.abs(speed), AMP_MIN, AMP_MAX); }
    function canMove(d) {
        const s = speedNow();
        return d > 0 ? s < MAX_SPEED : s > -MAX_SPEED;
    }
                while (arc < want) {
                    const sl = waveSlope(x);
                    const ds = Math.min(4, want - arc);
                    x += ds / Math.sqrt(1 + sl * sl);
                    arc += ds;
                }
                if (x > W + 40) {
                    done = true;
                    break;
                }
                ctx.save();
                ctx.translate(x, waveY(x));
                ctx.rotate(Math.atan(waveSlope(x)));

Friction, Spring Stiffness, Lens Strength — The Numbers That Decide How the Track Glides

Every scene was captured as 24 frames across two seconds, and the three columns below come from those captures. Area swept is the share of the screen that motion passed over, mean intensity is the average pixel difference between frames, and frames with motion counts how many of the 23 frame gaps showed change. Eight scenes clear 30% area swept, 06 sits just under at 28.8%, and none drops below 10%.

Scene Area swept Mean intensity Frames with motion
01 Wheel inertia track 41.4% 109.7 16/23
02 Pinned panels 50.4% 94.1 23/23
03 Fling snap 46.5% 83.3 21/23
04 Folding screen 41.9% 59.5 21/23
05 Parallax ridges 37.1% 39.3 23/23
06 Cylinder gallery 28.8% 87.3 17/23
07 Cord cards 50.2% 119.4 23/23
08 Edge lens 42.5% 72.6 22/23
09 Wave ribbon 30.3% 84.8 23/23

Friction is the ratio of a geometric series

glideDistance in 01 multiplies velocity by FRICTION f on each step, so the total glide is the geometric series v₀·f/(1−f). Plug in the 12px-per-step v₀ from one wheel notch and f 0.92, and the continuous result is 138px, while the 16ms-step simulation gives 149.4px (1056ms), the discrete steps running slightly longer. speedFor solves the same formula in reverse so one key press covers exactly 132px.

A stiffer spring overshoots more

The spring in 03 has just two terms, a = SNAP_K·(target−x) − SNAP_C·v. At K 120 and C 14 the damping ratio is 0.639, with 5.5% overshoot and a 912ms settle; raise only K to 320 and the natural frequency climbs by √(320/120) ≈ 1.63, but the damping ratio sinks to 0.391 and overshoot grows to 24.1%. That one number decides whether the snap feels firm or bouncy.

The lens curve squeezes the edges

The sample position in 08, s = u·(1+K·|u|²), reads the source unchanged at the screen center (u 0) and packs in 1+K times as much at the edges (u ±1). Magnification is the reciprocal of its derivative, 1/(1+3K·|u|²), so at K 0.35 the edge scale works out to 0.488, the flat middle band to 156px and the source the screen holds to 648px, with the source pixels read only once at start through getImageData.

Trackpads Mixed With Wheels, Dozens of Cards, a Hidden Tab — Where the Sideways Track Breaks

Each point below was traced in the source or confirmed with a probe while building the nine scenes.

A trackpad and a wheel do not meet at the same scale

Separately from wheelPx choosing the larger axis, a trackpad's sideways swipe delivers deltaX in the tens to hundreds of px per event, far faster than one mouse wheel notch (100 in these demos) at the same gain. The usual fix is to lower the gain or cap the travel per step; set WHEEL_GAIN high and a single notch skips half the track.

When the page gets its vertical scroll back

When canMove(d) returns false toward an end, the listener returns without calling preventDefault, and the wheel event flows on to the page so vertical scrolling continues; the pass and blocked results from the probe are the evidence for that design. Scene 09 adds one more exception, leaving the event alone once speed already sits at ±720 (±MAX_SPEED), and in 02 the pin lets go the moment progress reaches 0 or 1, handing the wheel to the page. Copy only the listener and call preventDefault unconditionally, and a reader whose pointer rests on the track can no longer wheel the page past it.

Three things that give way first with dozens of cards

In 01, MAX_X grows with the track, so the progress bar thumb, (W−48)·W/(MAX_X+W), shrinks from 128px at 12 cards to 39px at 40, and with hundreds of cards it sits at its 10px floor. In 06, on screens at dpr 3 and above, filling twelve polygon cards gets more expensive, and 08 remaps 144,000 pixels per redraw, so its cost rises linearly as the stage grows; its source width SRC_W is also fixed at 1730, so on a wider stage the right end fills with background color.

Time spent in a hidden tab is dropped

Each frame passes along at most 100ms and runs at most six steps at once, so a scene left in a hidden tab for a long time does not race when you return. The tradeoff is that the hidden time is discarded rather than caught up.

Values past their limit break things at once

Raise FRICTION to 1 or more and the series diverges so the track keeps moving; push MAX_FOLD in 04 past 90 degrees and the folded faces overlap; in 06, letting RADIUS reach CAM_D creates angles where the projection denominator drops to zero or below. A zero among the depth values of 05 makes the travel division blow up, and setting SLACK in 07 to 1.0 or less leaves the cord without sag to carry the cards. At the 58 height cap in 09 letters tilt up to 54.5 degrees and readability collapses, and in 03 a canceled pointer (pointercancel) still leads into release, so the spring settles the track.

To change these constants yourself and watch which scene gives way first, open the zip; the password it asks for is gfvx7uu4.

Trying Three Values, Plus Accessibility for a Screen That Flows Sideways

Change Value What you see
Change the wheel inertia friction 01 FRICTION 0.92 → 0.97 The glide grows from 149.4px over 1056ms to 398.3px over 2880ms, about 2.7 times, and the series multiple moves from 11.5 to 32.7.
Change the snap spring stiffness 03 SNAP_K 120 → 320 Damping ratio 0.639 → 0.391, overshoot 5.5% → 24.1%, while the settle time barely moves (912ms → 880ms).
Change the edge lens strength 08 LENS_K 0.35 → 0.8 Edge scale 0.488 → 0.294 (tile width 44.9 → 27.1px), flat band 156 → 104px, source held 648 → 864px.

These figures come from calling the source functions directly in Node and simulating them, not from the render, so read them apart from the capture table above.

All nine scenes can be driven from the keyboard alone: every <canvas> carries tabindex="0" and an aria-label naming the scene, and ←/→ move every track. On top of that, 02 also takes ↑/↓, Home/End work in the eight scenes from 01 to 08, and 09 answers to ←/→ only. Each scene has one or two buttons, and the lens toggle in 08 and the pause toggle in 09 report their state through aria-pressed. The status pill's aria-live stays off during the auto demo and switches to polite the first time a person touches the scene (probe, 9/9). For anyone with prefers-reduced-motion turned on, the requestAnimationFrame loop does not start and a still frame is drawn once, yet wheel, key, drag and button input still apply at once (probe, 9/9; 07 ignores only the shake). On the CSS side the reduce block is a single line that removes the button transition.

Does Horizontal Scroll CSS Mouse Wheel Handling Work Without JavaScript?

For these scenes, the honest answer is no. The moment a wheel's vertical value becomes sideways travel, a script has to listen for wheel and call preventDefault conditionally, and inertia, snapping, projection and the lens are per-frame calculations that a CSS declaration cannot express. Only the shape of 02, which maps vertical progress onto a sideways position, falls partly within CSS reach; the CSS-only slider and carousel post is its neighbor, and the other eight stay in canvas-script territory. For more scroll physics, continue with scroll momentum physics patterns, and for layered backgrounds, see CSS parallax scroll.

Questions People Ask Before Turning Horizontal Scroll Website Examples Into Their Own Page

Which horizontal scroll css example in this set is built for dragging?

Scene 03 is the one built around drag and fling: it predicts the landing slot from the release speed and springs into it. Every scene except 02 also accepts pointer drags, and 01 carries the drag speed at release (clamped to ±60) on as inertia, but only 03 uses that speed to choose a landing slot in advance.

Will it keep working if I add many more cards?

Not as is; the constants need to be set again. In 01 the progress bar thumb drops from 128px at 12 cards to 39px at 40 and bottoms out at 10px, and in 08 the source width SRC_W is computed from the tile count (1730px at 16 tiles), so it grows with the row, but a stage wider than that leaves its right side in background color. Scenes with a fixed slot width such as 03 only need the card count raised.

How does touch on mobile split from vertical scrolling?

The canvas sets touch-action: pan-y, so a sideways finger drag goes to the scene and a vertical swipe stays with the browser (9/9). Since the page's vertical scroll is left alone, the scenes can sit inside the normal flow of an article.

Enter the archive password

The password is inside this article. You will find it as you read.