CSS Scroll-Driven Animations: Progress Timelines and ViewTimeline

CSS Scroll-Driven Animations (now baseline in Chrome 115+ and Edge 115+, with Firefox support landing) let you link an animation’s progress directly to scroll position — either the page scroll (scroll() timeline) or an element’s visibility in the viewport (view() timeline) — entirely in CSS, with no JavaScript needed.

Problem: Scroll-triggered animations in WordPress themes — progress bars, sticky headers, parallax effects — require JavaScript scroll event listeners, IntersectionObserver, and manual timeline calculations that are brittle and performance-intensive.

Solution: Use CSS Scroll-Driven Animations — attach an animation to a scroll timeline with animation-timeline: scroll() for page-level progress or animation-timeline: view() for element visibility. Combine with animation-range to start and end the animation at specific scroll positions relative to the element entering or leaving the viewport.


The examples below show a reading-progress bar driven by page scroll, a fade-in animation triggered as elements enter the viewport, and a parallax effect using animation-timeline: scroll() with named scroll timelines.


/* ── 1. Reading-progress bar (scroll timeline) ────────────────────────── */
@keyframes grow-bar {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
}

.reading-progress {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    height: 4px;
    background: #0073aa;
    transform-origin: left;

    animation: grow-bar linear;
    animation-timeline: scroll(root block);   /* root = , block axis */
}

/* ── 2. Fade-in as element enters viewport (view timeline) ──────────────── */
@keyframes fade-up {
    from { opacity: 0; translate: 0 2rem; }
    to   { opacity: 1; translate: 0 0;    }
}

.reveal {
    animation: fade-up 0.5s ease-out both;
    animation-timeline: view();
    /* start when element is 20% into view, finish when 60% is visible */
    animation-range: entry 20% entry 60%;
}

/* ── 3. Named scroll timeline for parallax ──────────────────────────────── */
.scroll-container {
    overflow-y: scroll;
    scroll-timeline: --page-scroll block;   /* name the timeline */
}

@keyframes parallax-shift {
    from { transform: translateY(0); }
    to   { transform: translateY(-80px); }
}

.hero-background {
    animation: parallax-shift linear both;
    animation-timeline: --page-scroll;
    animation-range: 0% 50%;    /* animate over first half of scroll */
}

/* ── 4. Staggered entry animations using view() ─────────────────────────── */
.card-grid > .card {
    animation: fade-up 0.4s ease-out both;
    animation-timeline: view();
    animation-range: entry 0% entry 50%;
}

.card-grid > .card:nth-child(2) { animation-delay: 0.1s; }
.card-grid > .card:nth-child(3) { animation-delay: 0.2s; }
.card-grid > .card:nth-child(4) { animation-delay: 0.3s; }


NOTE: Wrap scroll-driven animations in a @supports (animation-timeline: scroll()) block and provide a non-animated fallback — Firefox shipped support in version 132 (Dec 2024) but users on older versions will see unanimated content if you do not guard against it.