Tuesday, October 22, 2024
HomeProgrammingUnleash The Energy Of Scroll-Pushed Animations

Unleash The Energy Of Scroll-Pushed Animations


I’m completely behind in studying about scroll-driven animations other than the “studying progress bar” experiments throughout CodePen. Properly, I’m not precisely “inexperienced” on the subject; we’ve revealed a handful of articles on it together with this neat-o one by Lee Meyer revealed the opposite week.

Our “oldest” article concerning the characteristic is by Bramus, dated again to July 2021. We had been calling it “scroll-linked” animation again then. I particularly point out Bramus as a result of there’s nobody else working as onerous as he’s to find sensible use instances the place scroll-pushed animations shine whereas serving to everybody perceive the idea. He writes about it exhaustively on his private weblog along with writing the Chrome for Builders documentation on it.

However there’s additionally this free course he calls “Unleash the Energy of Scroll-Pushed Animations” revealed on YouTube as a sequence of 10 brief movies. I made a decision it was excessive time to sit down, watch, and study from among the finest. These are my notes from it.


Introduction

  • A scroll-driven animation is an animation that responds to scrolling. There’s a direct hyperlink between scrolling progress and the animation’s progress.
  • Scroll-pushed animations are completely different than scroll-triggered animations, which execute on scroll and run of their entirety. Scroll-driven animations pause, play, and run with the course of the scroll. It sounds to me like scroll-triggered animations are lots just like the CSS model of the JavaScript intersection observer that fires and performs independently of scroll.
  • Why study this? It’s tremendous simple to take an current CSS animation or a WAAPI animation and hyperlink it as much as scrolling. The one “new” factor to study is the way to connect an animation to scrolling. Plus, hey, it’s the platform!
  • There are additionally efficiency perks. JavsScript libraries that set up scroll-driven animations usually reply to scroll occasions on the primary thread, which is render-blocking… and JANK! We’re working with hardware-accelerated animations… and NO JANK. Yuriko Hirota has a case research on the efficiency of scroll-driven animations revealed on the Chrome weblog.
  • Supported in Chrome 115+. Can use @helps (animation-timeline: scroll()). Nevertheless, I not too long ago noticed Bramus publish an replace saying we have to search for animation-range help as nicely.
@helps ((animation-timeline: scroll()) and (animation-range: 0% 100%)) {
  /* Scroll-Pushed Animations associated types go right here */
  /* This test excludes Firefox Nightly which solely has a partial implementation for the time being of posting (mid-September 2024). */
}
  • Keep in mind to make use of prefers-reduced-motion and be aware of those that might not need them.

Core Ideas: scroll() and ScrollTimeline

Let’s take an current CSS animation.

@keyframes grow-progress {
  from {
    rework: scaleX(0);
  }
  to {
    rework: scaleX(1);
  }
}

#progress {
  animation: grow-progress 2s linear forwards;
}

Translation: Begin with no width and scale it to its full width. When utilized, it takes two seconds to finish and strikes with linear easing simply within the forwards course.

This simply runs when the #progress aspect is rendered. Let’s connect it to scrolling.

  • animation-timeline: The timeline that controls the animation’s progress.
  • scroll(): Creates a brand new scroll timeline set as much as observe the closest ancestor scroller within the block course.
#progress {
  animation: grow-progress 2s linear forwards;
  animation-timeline: scroll();
}

That’s it! We’re linked up. Now we are able to take away the animation-duration worth from the combination (or set it to auto):

#progress {
  animation: grow-progress linear forwards;
  animation-timeline: scroll();
}

Notice that we’re unable to plop the animation-timeline property on the animation shorthand, at the very least for now. Bramus calls it a “reset-only sub-property of the shorthand” which is a brand new time period to me. Its worth will get reset whenever you use the shorthand the identical approach background-color is reset by background. Meaning the very best follow is to declare animation-timeline after animation.

/* YEP! */
#progress {
  animation: grow-progress linear forwards;
  animation-timeline: scroll();
}

/* NOPE! */
#progress {
  animation-timeline: scroll();
  animation: grow-progress linear forwards;
}

Let’s discuss concerning the scroll() perform. It creates an nameless scroll timeline that “walks up” the ancestor tree from the goal aspect to the closest ancestor scroll. On this instance, the closest ancestor scroll is the :root aspect, which is tracked within the block course.

Showing the relationship between an element and its scrolling ancestor.

We are able to identify scroll timelines, however that’s in one other video. For now, know that we are able to alter which axis to trace and which scroller to focus on within the scroll() perform.

animation-timeline: scroll(<axis> <scroller>);
  • <axis>: The axis — be it block (default), inline, y, or x.
  • <scroller>: The scroll container aspect that defines the scroll place that influences the timeline’s progress, which might be nearest (default), root (the doc), or self.

If the basis aspect doesn’t have an overflow, then the animation turns into inactive. WAAPI offers us a option to set up scroll timelines in JavaScript with ScrollTimeline.

const $progressbar = doc.querySelector(#progress);

$progressbar.type.transformOrigin = '0% 50%';
$progressbar.animate(
  {
    rework: ['scaleX(0)', 'scaleY()'],
  },
  {
    fill: 'forwards',
    timeline: new ScrollTimeline({
      supply: doc.documentElement, // root aspect
      // can management `axis` right here as nicely
    }),
  }
)

Core Ideas: view() and ViewTimeline

First, we oughta distinguish a scroll container from a scroll port. Overflow might be seen or clipped. Clipped might be scrolling.

Diagram showing scrollport, scroll container, and scrollable overflow.

These two bordered containers present how simple it’s to conflate scrollports and scroll containers. The scrollport is the seen half and coincides with the scroll container’s padding-box. When a scrollbar is current, that plus the scroll container is the basis scroller, or the scroll container.

Diagram showing the root scroller.

A view timeline tracks the relative place of a topic inside a scrollport. Now we’re moving into IntersectionObserver territory! So, for instance, we are able to start an animation on the scroll timeline when a component intersects with one other, such because the goal aspect intersecting the viewport, then it progresses with scrolling.

Bramus walks via an instance of animating photographs in long-form content material after they intersect with the viewport. First, a CSS animation to disclose a picture from zero opacity to full opacity (with some added clipping).

@keyframes reveal {
  from {
    opacity: 0;
    clip-path: inset(45% 20% 45% 20%);
  }
  to {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }
}

.revealing-image {
  animation: reveal 1s linear each;
}

This presently runs on the doc’s timeline. Within the final video, we used scroll() to register a scroll timeline. Now, let’s use the view() perform to register a view timeline as a substitute. This fashion, we’re responding to when a .revealing-image aspect is in, nicely, view.

.revealing-image {
  animation: reveal 1s linear each;
  /* Rember to declare the timeline after the shorthand */
  animation-timeline: view();
}

At this level, nonetheless, the animation is sweet however solely completes when the aspect absolutely exits the viewport, that means we don’t get to see the whole factor. There’s a really useful option to repair this that Bramus will cowl in one other video. For now, we’re dashing up the keyframes as a substitute by finishing the animation on the 50% mark.

@keyframes reveal {
  from {
    opacity: 0;
    clip-path: inset(45% 20% 45% 20%);
  }
  50% {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }
}

Extra on the view() perform:

animation-timeline: view(<axis> <view-timeline-inset>);

We all know <axis> from the scroll() perform — it’s the identical deal. The <view-timeline-inset> is a approach of adjusting the visibility vary of the view progress (what a mouthful!) that we are able to set to auto (default) or a <length-percentage>. A constructive inset strikes in an outward adjustment whereas a unfavorable worth strikes in an inward adjustment. And spot that there isn’t any <scroller> argument — a view timeline at all times tracks its topic’s nearest ancestor scroll container.

OK, transferring on to adjusting issues with ViewTimeline in JavaScript as a substitute.

const $photographs = doc.querySelectorAll(.revealing-image);

$photographs.forEach(($picture) => {
  $picture.animate(
    [
      { opacity: 0, clipPath: 'inset(45% 20% 45% 20%)', offset: 0 }
      { opacity: 1; clipPath: 'inset(0% 0% 0% 0%)', offset: 0.5 }
    ],
    {
      fill: 'each',
      timeline: new ViewTimeline({
        topic: $picture,
        axis: 'block', // Do now we have to do that if it is the default?
      }),
    }
  }
)

This has the identical impact because the CSS-only method with animation-timeline.

Timeline Ranges Demystified

Final time, we adjusted the place the picture’s reveal animation ends by tweaking the keyframes to finish at 50% quite than 100%. We may have performed with the inset(). However there may be a neater approach: alter the animation attachment vary,

Most scroll animations go from zero scroll to 100% scroll. The animation-range property adjusts that:

animation-range: regular regular;

These two values: the beginning scroll and finish scroll, default:

animation-range: 0% 100%;

Different size items, after all:

animation-range: 100px 80vh;

The instance we’re taking a look at is a “full-height cowl card to mounted header”. Mouthful! But it surely’s neat, going from an immersive full-page header to a skinny, mounted header whereas scrolling down the web page.

@keyframes sticky-header {
  from {
    background-position: 50% 0;
    peak: 100vh;
    font-size: calc(4vw + 1em);
  }
  to {
    background-position: 50% 100%;
    peak: 10vh;
    font-size: calc(4vw + 1em);
    background-color: #0b1584;
  }
}

If we run the animation throughout scroll, it takes the total animation vary, 0%-100%.

.sticky-header {
  place: mounted;
  prime: 0;

  animation: sticky-header linear forwards;
  animation-timeline: scroll();
}

Just like the revealing photographs from the final video, we wish the animation vary somewhat narrower to forestall the header from animating out of view. Final time, we adjusted the keyframes. This time, we’re going with the property method:

.sticky-header {
  place: mounted;
  prime: 0;

  animation: sticky-header linear forwards;
  animation-timeline: scroll();
  animation-range: 0vh 90vh;
}

We needed to subtract the total peak (100vh) from the header’s eventual peak (10vh) to get that 90vh worth. I can’t imagine that is taking place in CSS and never JavaScript! Bramus sagely notes that font-size animation occurs on the primary thread — it isn’t hardware-accelerated — and the whole scroll-driven animation runs on the primary because of this. Different properties trigger this as nicely, notably customized properties.

Again to the animation vary. It may be diagrammed like this:

Visual demo showing the animation's full range.
The animation “cowl vary”. The dashed space represents the peak of the animated goal aspect.

Discover that there are 4 factors in there. We’ve solely been chatting concerning the “begin edge” and “finish edge” up thus far, however the vary covers a bigger space in view timelines. So, this:

animation-range: 0% 100%; /* similar as 'regular regular' */

…to this:

animation-range: cowl 0% cowl 100%; /* 'cowl regular cowl regular' */

…which is admittedly this:

animation-range: cowl;

So, yeah. That revealing picture animation from the final video? We may have carried out this, quite than fuss with the keyframes or insets:

animation-range: cowl 0% cowl 50%;

So good. The demo visualization is hosted at scroll-driven-animations.type. Oh, and now we have key phrase values out there: comprise, entry, exit, entry-crossing, and exit-crossing.

Showing a contained animation range.
comprise
Showing an entry animation range.
entry
Showing an exit animation range.
exit

The examples up to now are primarily based on the scroller being the basis aspect. What about ranges which are taller than the scrollport topic? The ranges change into barely completely different.

An element larger than the scrollport where contain equals 100% when out of range but 0% before it actually reaches the end of the animation.
Simply have to pay attention to the aspect’s measurement and the way it impacts the scrollport.

That is the place the entry-crossing and entry-exit values come into play. It is a little mind-bendy at first, however I’m positive it’ll get simpler with use. It’s clear issues can get complicated actually rapidly… which is particularly true after we begin working with a number of scroll-driven animation with their very own animation ranges. Sure, that’s all doable. It’s all good so long as the ranges don’t overlap. Bramus makes use of a contact checklist demo the place contact objects animate after they enter and exit the scrollport.

@keyframes animate-in {
  0% { opacity: 0; rework: translateY: 100%; }
  100% { opacity: 1; rework: translateY: 0%; }
}
@keyframes animate-out {
  0% { opacity: 1; rework: translateY: 0%; }
  100% { opacity: 0; rework: translateY: 100%; }
}

.list-view li {
  animation: animate-in linear forwards,
             animate-out linear forwards;
  animation-timeline: view();
  animation-range: entry, exit; /* animation-in, animation-out */
}

One other approach, utilizing entry and exit key phrases immediately within the keyframes:

@keyframes animate-in {
  entry 0% { opacity: 0; rework: translateY: 100%; }
  entry 100% { opacity: 1; rework: translateY: 0%; }
}
@keyframes animate-out {
  exit 0% { opacity: 1; rework: translateY: 0%; }
  exit 100% { opacity: 0; rework: translateY: 100%; }
}

.list-view li {
  animation: animate-in linear forwards,
             animate-out linear forwards;
  animation-timeline: view();
}

Discover that animation-range is not wanted since its values are declared within the keyframes. Wow.

OK, ranges in JavaScript.:

const timeline = new ViewTimeline({
  subjext: $li,
  axis: 'block',
})

// Animate in
$li.animate({
  opacity: [ 0, 1 ],
  rework: [ 'translateY(100%)', 'translateY(0)' ],
}, {
  fill: 'forwards',
  // One timeline occasion with a number of ranges
  timeline,
  rangeStart: 'entry: 0%',
  rangeEnd: 'entry 100%',
})

Core Ideas: Timeline Lookup and Named Timelines

This time, we’re studying the way to connect an animation to any scroll container on the web page while not having to be an ancestor of that aspect. That’s all about named timelines.

However first, nameless timelines observe their nearest ancestor scroll container.

<html> <!-- scroll -->
  <physique>
    <div class="wrapper">
      <div type="animation-timeline: scroll();"></div>
    </div>
  </physique>
</html>

Some issues may occur like when overflow is hidden from a container:

<html> <!-- scroll -->
  <physique>
    <div class="wrapper" type="overflow: hidden;"> <!-- scroll -->
      <div type="animation-timeline: scroll();"></div>
    </div>
  </physique>
</html>

Hiding overflow signifies that the aspect’s content material block is clipped to its padding field and doesn’t present any scrolling interface. Nevertheless, the content material should nonetheless be scrollable programmatically that means that is nonetheless a scroll container. That’s a straightforward gotcha if there ever was one! The higher route is to make use of overflow: clipped quite than hidden as a result of that forestalls the aspect from changing into a scroll container.

Hiding oveflow = scroll container. Clipping overflow = no scroll container. Bramus says he not sees any want to make use of overflow: hidden as of late except you explicitly have to set a scroll container. I would want to alter my muscle reminiscence to make that my go-to for hiding clipping overflow.

One other funky factor to observe for: absolute positioning on a scroll animation goal in a relatively-positioned container. It is going to by no means match an outdoor scroll container that’s scroll(inline-nearest) since it’s absolute to its container prefer it’s unable to see out of it.

We don’t should depend on the “nearest” scroll container or fuss with completely different overflow values. We are able to set which container to trace with named timelines.

.gallery {
  place: relative;
}
.gallery__scrollcontainer {
  overflow-x: scroll;
  scroll-timeline-name: --gallery__scrollcontainer;
  scroll-timeline-axis: inline; /* container scrolls within the inline course */
}
.gallery__progress {
  place: absolute;
  animation: progress linear forwards;
  animation-timeline: scroll(inline nearest);
}

We are able to shorten that up with the scroll-timeline shorthand:

.gallery {
  place: relative;
}
.gallery__scrollcontainer {
  overflow-x: scroll;
  scroll-timeline: --gallery__scrollcontainer inline;
}
.gallery__progress {
  place: absolute;
  animation: progress linear forwards;
  animation-timeline: scroll(inline nearest);
}

Notice that block is the scroll-timeline-axis preliminary worth. Additionally, observe that the named timeline is a dashed-ident, so it seems like a CSS variable.

That’s named scroll timelines. The identical is true of named view timlines.

.scroll-container {
  view-timeline-name: --card;
  view-timeline-axis: inline;
  view-timeline-inset: auto;
  /* view-timeline: --card inline auto */
}

Bramus confirmed a demo that recreates Apple’s outdated cover-flow sample. It runs two animations, one for rotating photographs and one for setting a picture’s z-index. We are able to connect each animations to the identical view timeline. So, we go from monitoring the closest scroll container for every aspect within the scroll:

.covers li {
  view-timeline-name: --li-in-and-out-of-view;
  view-timeline-axis: inline;

  animation: adjust-z-index linear each;
  animation-timeline: view(inline);
}
.playing cards li > img {
   animation: rotate-cover linear each;
   animation-timeline: view(inline);
}

…and easily reference the identical named timelines:

.covers li {
  view-timeline-name: --li-in-and-out-of-view;
  view-timeline-axis: inline;

  animation: adjust-z-index linear each;
  animation-timeline: --li-in-and-out-of-view;;
}
.playing cards li > img {
   animation: rotate-cover linear each;
   animation-timeline: --li-in-and-out-of-view;;
}

On this particular demo, the pictures rotate and scale however the up to date sizing doesn’t have an effect on the view timeline: it stays the identical measurement, respecting the unique field measurement quite than flexing with the modifications.

Phew, now we have one other device for attaching animations to timelines that aren’t direct ancestors: timeline-scope.

timeline-scope: --example;

This goes on an mum or dad aspect that’s shared by each the animated goal and the animated timeline. This fashion, we are able to nonetheless connect them even when they aren’t direct ancestors.

<div type="timeline-scope: --gallery">
  <div type="scroll-timeline: --gallery-inline;">
     ...
  </div>
  <div type="animation-timeline: --gallery;"></div>
</div>
Illustrating the relationship between a scroll target and container when they are not ancestors, but siblings.

It accepts a number of comma-separated values:

timeline-scope: --one, --two, --three;
/* or */
timeline-scope: all; /* Chrome 116+ */

There’s no Safari or Firefox help for the all kewword simply but however we are able to look ahead to it at Caniuse (or the newer BCD Watch!).

This video is taken into account the final one within the sequence of “core ideas.” The following 5 are extra targeted on use instances and examples.

Add Scroll Shadows to a Scroll Container

On this instance, we’re conditionally exhibiting scroll shadows on a scroll container. Chris calls scroll shadows one his favourite CSS-Methods of all time and we are able to nail them with scroll animations.

Right here is the demo Chris put collectively a couple of years in the past:

That depends on having a background with a number of CSS gradients which are pinned to the extremes with background-attachment: mounted on a single selector. Let’s modernize this, beginning with a unique method utilizing pseudos with sticky positioning:

.container::earlier than,
.container::after {
  content material: "";
  show: block;
  place: sticky;
  left: 0em; 
  proper 0em;
  peak: 0.75rem;

  &::earlier than {
    prime: 0;
    background: radial-gradient(...);
  }
  
  &::after {
    backside: 0;
    background: radial-gradient(...);
  }
}

The shadows fade out and in with a CSS animation:

@keyframes reveal {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

.container {
  overflow:-y auto;
  scroll-timeline: --scroll-timeline block; /* do we want `block`? */

  &::earlier than,
  &::after {
    animation: reveal linear each;
    animation-timeline: --scroll-timeline;
  }
}

This instance rocks a named timeline, however Bramus notes that an nameless one would work right here as nicely. Looks like nameless timelines are considerably fragile and named timelines are a great defensive technique.

The following factor we want is to set the animation’s vary so that every pseudo scrolls in the place wanted. Calculating the vary from the highest is pretty easy:

.container::earlier than {
  animation-range: 1em 2em;
}

The underside is somewhat tricker. It ought to begin when there are 2em of scrolling after which solely journey for 1em. We are able to merely reverse the animation and add somewhat calculation to set the vary primarily based on it’s backside edge.

.container::after {
  animation-direction: reverse;
  animation-range: calc(100% - 2em) calc(100% - 1em);
}

Nonetheless yet one more factor. We solely need the shadows to disclose after we’re in a scroll container. If, for instance, the field is taller than the content material, there isn’t any scrolling, but we get each shadows.

Shadows on the top and bottom edges of the content, but the content is shorter than the box height, resulting in the shadow being in the middle of the box.

That is the place the conditional half is available in. We are able to detect whether or not a component is scrollable and react to it. Bramus is speaking about an animation key phrase that’s new to me: detect-scroll.

@keyframes detect-scroll {
  from,
  to {
     --can-scroll: ; /* worth is a single area and acts as boolean */
  }
}

.container {
  animation: detect-scroll;
  animation-timeline: --scroll-timeline;
  animation-fill-mode: none;
}

Gonna should wrap my head round this… however the basic concept is that --can-scroll is a boolean worth we are able to use to set visibility on the pseudos:

.content material::earlier than,
.content material::after {
    --vis-if-can-scroll: var(--can-scroll) seen;
    --vis-if-cant-scroll: hidden;

  visibility: var(--vis-if-can-scroll, var(--vis-if-cant-scroll));
}

Bramus factors to this CSS-Methods article for extra on the conditional toggle stuff.

Animate Components in Completely different Instructions

This ought to be enjoyable! Let’s say now we have a set of columns:

<div class="columns">
  <div class="column reverse">...</div>
  <div class="column">...</div>
  <div class="column reverse">...</div>
</div>

The purpose is getting the 2 outer reverse columns to scroll within the reverse course because the inside column scrolls within the different course. Basic JavaScript territory!

The columns are arrange in a grid container. The columns flex within the column course.

/* run if the browser helps it */
@helps (animation-timeline: scroll()) {

  .column-reverse {
    rework: translateY(calc(-100% + 100vh));
    flex-direction: column-reverse; /* flows in reverse order */
  }

  .columns {
    overflow-y: clip; /* not a scroll container! */
  }

}
The bottom edge of the outer columns are aligned with the top edge of the viewport.

First, the outer columns are pushed all the way in which up so the underside edges are aligned with the viewport’s prime edge. Then, on scroll, the outer columns slide down till their prime edges re aligned with the viewport’s backside edge.

The CSS animation:

@keyframes adjust-position {
  from /* the highest */ {
    rework: translateY(calc(-100% + 100vh));
  }
  to /* the underside */ {
    rework: translateY(calc(100% - 100vh));
  }
}

.column-reverse {
  animation: adjust-position linear forwards;
  animation-timeline: scroll(root block); /* viewport in block course */
}

The method is comparable in JavaScript:

const timeline = new ScrollTimeline({
  supply: doc.documentElement,
});

doc.querySelectorAll(".column-reverse").forEach($column) => {
  $column.animate(
    {
      rework: [
        "translateY(calc(-100% + 100vh))",
        "translateY(calc(100% - 100vh))"
      ]
    },
    {
      fill: "each",
      timeline,
    }
  );
}

Animate 3D Fashions and Extra on Scroll

This one’s working with a customized aspect for a 3D mannequin:

<model-viewer alt="Robotic" src="https://css-tricks.com/unleash-the-power-of-scroll-driven-animations/robotic.glb"></model-viewer>

First, the scroll-driven animation. We’re attaching an animation to the part however not defining the keyframes simply but.

@keyframes foo {

}

model-viewer {
  animation: foo linear each;
  animation-timeline: scroll(block root); /* root scroller in block course */
}

There’s some JavaScript for the total rotation and orientation:

// Bramus made somewhat helper for dealing with the requested animation frames
import { trackProgress } from "https://esm.sh/@bramus/sda-utilities";

// Choose the part
const $mannequin = doc.QuerySelector("model-viewer");
// Animation begins with the primary iteration
const animation = $mannequin.getAnimations()[0];

// Variable to get the animation's timing data
let progress = animation.impact.getComputedTiming().progress * 1;
// If when completed, $progress = 1
if (animation.playState === "completed") progress = 1;
progress = Math.max(0.0, Math.min(1.0, progress)).toFixed(2);

// Convert this to levels
$mannequin.orientation = `0deg 0deg $(progress * -360)deg`;

We’re utilizing the impact to get the animation’s progress quite than the present timed spot. The present time worth is at all times measured relative to the total vary, so we want the impact to get the progress primarily based on the utilized animation.

Scroll Velocity Detection

The video description is useful:

Bramus goes full experimental and makes use of Scroll-Pushed Animations to detect the energetic scroll velocity and the directionality of scroll. Detecting this lets you type a component primarily based on whether or not the consumer is scrolling (or not scrolling), the course they’re scrolling in, and the velocity they’re scrolling with … and this all utilizing solely CSS.

First off, it is a hack. What we’re taking a look at is expermental and never very performant. We wish to detect the animations’s velocity and course. We begin with two customized properties.

@keyframes adjust-pos {
  from {
    --scroll-position: 0;
    --scroll-position-delayed: 0;
  }
  to {
    --scroll-position: 1;
    --scroll-position-delayed: 1;
  }
}

:root {
  animation: adjust-pos linear each;
  animation-timeline: scroll(root);
}

Let’s register these customized properties so we are able to interpolate the values:

@property --scroll-position {
  syntax: "<quantity>";
  inherits: true;
  initial-value: 0;
}

@property --scroll-position-delayed {
  syntax: "<quantity>";
  inherits: true;
  initial-value: 0;
}

As we scroll, these values change. If we add somewhat delay, then we are able to stagger issues a bit:

:root {
  animation: adjust-pos linear each;
  animation-timeline: scroll(root);
}

physique {
  transition: --scroll-position-delayed 0.15s linear;
}

The truth that we’re making use of this to the physique is a part of the trick as a result of it depends upon the parent-child relationship between html and physique. The mum or dad aspect updates the values instantly whereas the kid lags behind only a tad. The consider to the identical worth, however one is slower to start out.

We are able to use the distinction between the 2 values as they’re staggered to get the rate.

:root {
  animation: adjust-pos linear each;
  animation-timeline: scroll(root);
}

physique {
  transition: --scroll-position-delayed 0.15s linear;
  --scroll-velocity: calc(
    var(--scroll-position) - var(--scroll-position-delayed)
  );
}

Intelligent! If --scroll-velocity is the same as 0, then we all know that the consumer shouldn’t be scrolling as a result of the 2 values are in sync. A constructive quantity signifies the scroll course is down, whereas a unfavorable quantity signifies scrolling up,.

Showing values for the scroll position, the delayed position, and the velocity when scrolling occurs.

There’s somewhat discrepancy when scrolling abruptly modifications course. We are able to repair this by tighening the transition delay of --scroll-position-delayed however then we’re rising the rate. We’d want a multiplier to additional appropriate that… that’s why it is a hack. However now now we have a option to sniff the scrolling velocity and course!

Right here’s the hack utilizing math features:

physique {
  transition: --scroll-position-delayed 0.15s linear;
  --scroll-velocity: calc(
    var(--scroll-position) - var(--scroll-position-delayed)
  );
  --scroll-direction: signal(var(--scroll-velocity));
  --scroll-speed: abs(var(--scroll-velocity));
}

It is a little humorous as a result of I’m seeing that Chrome doesn’t but help signal() or abs(), at the very least on the time I’m watching this. Gotta allow chrome://flags. There’s a polyfill for the maths delivered to you by Ana Tudor proper right here on CSS-Methods.

Showing values for the scroll position, the delayed position, the velocity, the scroll direction, and the scroll speed when scrolling occurs.

So, now we may theoretically do one thing like skew a component by a certain quantity or give it a sure stage of background coloration saturation relying on the scroll velocity.

.field {
  rework: skew(calc(var(--scroll-velocity) * -25deg));
  transition: background 0.15s ease;
  background: hsl(
    calc(0deg + (145deg * var(--scroll-direction))) 50 % 50%
  );
}

We may do all this with type queries ought to we wish to:

@container type(--scroll-direction: 0) { /* idle */
  .slider-item {
    background: crimson;
  }
}
@container type(--scroll-direction: 1) { /* scrolling down */
  .slider-item {
    background: forestgreen;
  }
}
@container type(--scroll-direction: -1) { /* scrolling down */
  .slider-item {
    background: lightskyblue;
  }
}

Customized properties, scroll-driven animations, and magnificence queries — multi function demo! These are wild occasions for CSS, inform ya what.

Outro

The tenth and remaining video! Only a abstract of the sequence, so no new notes right here. However right here’s an amazing demo to cap it off.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments