Tag: Infinite

Efficient Infinite Utility Helpers Using Inline CSS Custom Properties and calc()

I recently wrote a very basic Sass loop that outputs several padding and margin utility classes. Nothing fancy, really, just a Sass map with 11 spacing values, looped over to create classes for both padding and margin on each side. As we’ll see, this works, but it ends up a pretty hefty amount of CSS. We’re going to refactor it to use CSS custom properties and make the system much more trim.

Here’s the original Sass implementation:

$  space-stops: (   '0': 0,   '1': 0.25rem,   '2': 0.5rem,   '3': 0.75rem,   '4': 1rem,   '5': 1.25rem,   '6': 1.5rem,   '7': 1.75rem,   '8': 2rem,   '9': 2.25rem,   '10': 2.5rem, );  @each $  key, $  val in $  space-stops {   .p-#{$  key} {     padding: #{$  val} !important;   }   .pt-#{$  key} {     padding-top: #{$  val} !important;   }   .pr-#{$  key} {     padding-right: #{$  val} !important;   }   .pb-#{$  key} {     padding-bottom: #{$  val} !important;   }   .pl-#{$  key} {     padding-left: #{$  val} !important;   }   .px-#{$  key} {     padding-right: #{$  val} !important;     padding-left: #{$  val} !important;   }   .py-#{$  key} {     padding-top: #{$  val} !important;     padding-bottom: #{$  val} !important;   }    .m-#{$  key} {     margin: #{$  val} !important;   }   .mt-#{$  key} {     margin-top: #{$  val} !important;   }   .mr-#{$  key} {     margin-right: #{$  val} !important;   }   .mb-#{$  key} {     margin-bottom: #{$  val} !important;   }   .ml-#{$  key} {     margin-left: #{$  val} !important;   }   .mx-#{$  key} {     margin-right: #{$  val} !important;     margin-left: #{$  val} !important;   }   .my-#{$  key} {     margin-top: #{$  val} !important;     margin-bottom: #{$  val} !important;   } } 

This very much works. It outputs all the utility classes we need. But, it can also get bloated quickly. In my case, they were about 8.6kb uncompressed and under 1kb compressed. (Brotli was 542 bytes, and gzip came in at 925 bytes.)

Since they are extremely repetitive, they compress well, but I still couldn’t shake the feeling that all these classes were overkill. Plus, I hadn’t even done any small/medium/large breakpoints which are fairly typical for these kinds of helper classes.

Here’s a contrived example of what the responsive version might look like with small/medium/large classes added. We’ll re-use the $ space-stops map defined previously and throw our repetitious code into a mixin

@mixin finite-spacing-utils($  bp: '') {     @each $  key, $  val in $  space-stops {         .p-#{$  key}#{$  bp} {             padding: #{$  val} !important;         }         .pt-#{$  key}#{$  bp} {             padding-top: #{$  val} !important;         }         .pr-#{$  key}#{$  bp} {             padding-right: #{$  val} !important;         }         .pb-#{$  key}#{$  bp} {             padding-bottom: #{$  val} !important;         }         .pl-#{$  key}#{$  bp} {             padding-left: #{$  val} !important;         }         .px-#{$  key}#{$  bp} {             padding-right: #{$  val} !important;             padding-left: #{$  val} !important;         }         .py-#{$  key}#{$  bp} {             padding-top: #{$  val} !important;             padding-bottom: #{$  val} !important;         }          .m-#{$  key}#{$  bp} {             margin: #{$  val} !important;         }         .mt-#{$  key}#{$  bp} {             margin-top: #{$  val} !important;         }         .mr-#{$  key}#{$  bp} {             margin-right: #{$  val} !important;         }         .mb-#{$  key}#{$  bp} {             margin-bottom: #{$  val} !important;         }         .ml-#{$  key}#{$  bp} {             margin-left: #{$  val} !important;         }         .mx-#{$  key}#{$  bp} {             margin-right: #{$  val} !important;             margin-left: #{$  val} !important;         }         .my-#{$  key}#{$  bp} {             margin-top: #{$  val} !important;             margin-bottom: #{$  val} !important;         }     } }  @include finite-spacing-utils;  @media (min-width: 544px) {     @include finite-spacing-utils($  bp: '_sm'); }  @media (min-width: 768px) {     @include finite-spacing-utils($  bp: '_md'); }  @media (min-width: 1024px) {     @include finite-spacing-utils($  bp: '_lg'); }  

That clocks in at about 41.7kb uncompressed (and about 1kb with Brotli, and 3kb with gzip). It still compresses well, but it’s a bit ridiculous.

I knew it was possible to reference data-* attributes from within CSS using the [attr() function, so I wondered if it was possible to use calc() and attr() together to create dynamically-calculated spacing utility helpers via data-* attributes — like data-m="1" or data-m="1@md" — then in the CSS to do something like margin: calc(attr(data-m) * 0.25rem) (assuming I’m using a spacing scale incrementing at 0.25rem intervals). That could be very powerful.

But the end of that story is: no, you (currently) can’t use attr() with any property except the content property. Bummer. But in searching for attr() and calc() information, I found this intriguing Stack Overflow comment by Simon Rigét that suggests setting a CSS variable directly within an inline style attribute. Aha!

So it’s possible to do something like <div style="--p: 4;"> then, in CSS:

:root {   --p: 0; }  [style*='--p:'] {   padding: calc(0.25rem * var(--p)) !important; } 

In the case of the style="--p: 4;" example, you’d effectively end up with padding: 1rem !important;.

… and now you have an infinitely scalable spacing utility class monstrosity helper.

Here’s what that might look like in CSS:

:root {   --p: 0;   --pt: 0;   --pr: 0;   --pb: 0;   --pl: 0;   --px: 0;   --py: 0;   --m: 0;   --mt: 0;   --mr: 0;   --mb: 0;   --ml: 0;   --mx: 0;   --my: 0; }  [style*='--p:'] {   padding: calc(0.25rem * var(--p)) !important; } [style*='--pt:'] {   padding-top: calc(0.25rem * var(--pt)) !important; } [style*='--pr:'] {   padding-right: calc(0.25rem * var(--pr)) !important; } [style*='--pb:'] {   padding-bottom: calc(0.25rem * var(--pb)) !important; } [style*='--pl:'] {   padding-left: calc(0.25rem * var(--pl)) !important; } [style*='--px:'] {   padding-right: calc(0.25rem * var(--px)) !important;   padding-left: calc(0.25rem * var(--px)) !important; } [style*='--py:'] {   padding-top: calc(0.25rem * var(--py)) !important;   padding-bottom: calc(0.25rem * var(--py)) !important; }  [style*='--m:'] {   margin: calc(0.25rem * var(--m)) !important; } [style*='--mt:'] {   margin-top: calc(0.25rem * var(--mt)) !important; } [style*='--mr:'] {   margin-right: calc(0.25rem * var(--mr)) !important; } [style*='--mb:'] {   margin-bottom: calc(0.25rem * var(--mb)) !important; } [style*='--ml:'] {   margin-left: calc(0.25rem * var(--ml)) !important; } [style*='--mx:'] {   margin-right: calc(0.25rem * var(--mx)) !important;   margin-left: calc(0.25rem * var(--mx)) !important; } [style*='--my:'] {   margin-top: calc(0.25rem * var(--my)) !important;   margin-bottom: calc(0.25rem * var(--my)) !important; }  

This is a lot like the first Sass loop above, but there’s no loop going 11 times — and yet it’s infinite. It’s about 1.4kb uncompressed, 226 bytes with Brotli, or 284 bytes gzipped.

If you wanted to extend this for breakpoints, the unfortunate news is that you can’t put the “@” character in CSS variable names (although emojis and other UTF-8 characters are strangely permitted). So you could probably set up variable names like p_sm or sm_p. You’d have to add some extra CSS variables and some media queries to handle all this, but it won’t blow up exponentially the way traditional CSS classnames created with a Sass for-loop do.

Here’s the equivalent responsive version. We’ll use a Sass mixin again to cut down the repetition:

:root {   --p: 0;   --pt: 0;   --pr: 0;   --pb: 0;   --pl: 0;   --px: 0;   --py: 0;   --m: 0;   --mt: 0;   --mr: 0;   --mb: 0;   --ml: 0;   --mx: 0;   --my: 0; }  @mixin infinite-spacing-utils($  bp: '') {     [style*='--p#{$  bp}:'] {         padding: calc(0.25rem * var(--p)) !important;     }     [style*='--pt#{$  bp}:'] {         padding-top: calc(0.25rem * var(--pt)) !important;     }     [style*='--pr#{$  bp}:'] {         padding-right: calc(0.25rem * var(--pr)) !important;     }     [style*='--pb#{$  bp}:'] {         padding-bottom: calc(0.25rem * var(--pb)) !important;     }     [style*='--pl#{$  bp}:'] {         padding-left: calc(0.25rem * var(--pl)) !important;     }     [style*='--px#{$  bp}:'] {         padding-right: calc(0.25rem * var(--px)) !important;         padding-left: calc(0.25rem * var(--px)) !important;     }     [style*='--py#{$  bp}:'] {         padding-top: calc(0.25rem * var(--py)) !important;         padding-bottom: calc(0.25rem * var(--py)) !important;     }     [style*='--m#{$  bp}:'] {         margin: calc(0.25rem * var(--m)) !important;     }     [style*='--mt#{$  bp}:'] {         margin-top: calc(0.25rem * var(--mt)) !important;     }     [style*='--mr#{$  bp}:'] {         margin-right: calc(0.25rem * var(--mr)) !important;     }     [style*='--mb#{$  bp}:'] {         margin-bottom: calc(0.25rem * var(--mb)) !important;     }     [style*='--ml#{$  bp}:'] {         margin-left: calc(0.25rem * var(--ml)) !important;     }     [style*='--mx#{$  bp}:'] {         margin-right: calc(0.25rem * var(--mx)) !important;         margin-left: calc(0.25rem * var(--mx)) !important;     }     [style*='--my#{$  bp}:'] {         margin-top: calc(0.25rem * var(--my)) !important;         margin-bottom: calc(0.25rem * var(--my)) !important;     } }  @include infinite-spacing-utils;  @media (min-width: 544px) {     @include infinite-spacing-utils($  bp: '_sm'); }  @media (min-width: 768px) {     @include infinite-spacing-utils($  bp: '_md'); }  @media (min-width: 1024px) {     @include infinite-spacing-utils($  bp: '_lg'); }  

That’s about 6.1kb uncompressed, 428 bytes with Brotli, and 563 with gzip.

Do I think that writing HTML like <div style="--px:2; --my:4;"> is pleasing to the eye, or good developer ergonomics… no, not particularly. But could this approach be viable in situations where you (for some reason) need extremely minimal CSS, or perhaps no external CSS file at all? Yes, I sure do.

It’s worth pointing out here that CSS variables assigned in inline styles do not leak out. They’re scoped only to the current element and don’t change the value of the variable globally. Thank goodness! The one oddity I have found so far is that DevTools (at least in Chrome, Firefox, and Safari) do not report the styles using this technique in the “Computed” styles tab.

Also worth mentioning is that I’ve used good old padding  and margin properties with -top, -right, -bottom, and -left, but you could use the equivalent logical properties like padding-block and padding-inline. It’s even possible to shave off just a few more bytes by selectively mixing and matching logical properties with traditional properties. I managed to get it down to 400 bytes with Brotli and 521 with gzip this way.

Other use cases

This seems most appropriate for things that are on a (linear) incremental scale (which is why padding and margin seems like a good use case) but I could see this potentially working for widths and heights in grid systems (column numbers and/or widths). Maybe for typographic scales (but maybe not).

I’ve focused a lot on file size, but there may be some other uses here I’m not thinking of. Perhaps you wouldn’t write your code in this way, but a critical CSS tool could potentially refactor the code to use this approach.

Digging deeper

As I dug deeper, I found that Ahmad Shadeed blogged in 2019 about mixing calc() with CSS variable assignments within inline styles particularly for avatar sizes. Miriam Suzanne’s article on Smashing Magazine in 2019 didn’t use calc() but shared some amazing things you can do with variable assignments in inline styles.

The post Efficient Infinite Utility Helpers Using Inline CSS Custom Properties and calc() appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , , , , , ,

Going “Meta GSAP”: The Quest for “Perfect” Infinite Scrolling

I‘m not sure how this one came about. But, it‘s a story. This article is more about grokking a concept, one that’s going to help you think about your animations in a different way. It so happens that this particular example features infinite scrolling — specifically the “perfect” infinite scroll for a deck of cards without duplicating any of them.

Why am I here? Well, this all started from a tweet. A tweet that got me thinking about layouts and side-scrolling content.

I took that concept and used it on my site. And it’s still there in action at the time of writing.

Then I got to thinking more about gallery views and side-scrolling concepts. We hopped on a livestream and decided to try and make something like the old Apple “Cover Flow” pattern. Remember it?

My first thoughts for making this assumed I‘d make this so it works without JavaScript, as it does in the demo above, in a way that uses “progressive enhancement.” I grabbed Greensock and ScrollTrigger, and off we went. I came away from that work pretty disappointed. I had something but couldn‘t quite get infinite scrolling to work how the way I wanted. The “Next” and “Previous” buttons didn’t want to play ball. You can see it here, and it requires horizontal scrolling.

So I opened up a new thread on the Greensock forum. Little did I know I was about to open myself up to some serious learning! We solved the issue with the buttons. But, being me, I had to ask whether something else was possible. Was there a “clean” way to do infinite scrolling? I‘d tried something on stream but had no luck. I was curious. I’d tried a technique like that used in this pen which I created for the ScrollTrigger release.

The initial answer was that it is kinda tricky to do:

The hard part about infinite things on scroll is that the scroll bar is limited while the effect that you’re wanting is not. So you have to either loop the scroll position like this demo (found in the ScrollTrigger demos section) or hook directly into the scroll-related navigation events (like the wheel event) instead of actually using the actual scroll position.

I figured that was the case and was happy to leave it “as-is.” A couple of days passed and Jack dropped a reply that kinda blew my mind when I started digging into it. And now, after a bunch of going through it, I’m here to share the technique with you.

Animate anything

One thing that is often overlooked with GSAP, is that you can animate almost anything with it. This is often because visual things are what spring to mind when thinking about animation — the actual physical movement of something. Our first thought isn’t about taking that process to a meta-level and animating from a step back.

But, think about animation work on a larger scale and then break it down into layers. For example, you play a cartoon. The cartoon is a collection of compositions. Each composition is a scene. And then you have the power to scrub through that collection of compositions with a remote, whether it’s on YouTube, using your TV remote, or whatever. There are almost three levels to what is happening.

And this is the trick we need for creating different types of infinite loops. This is the main concept right here. We animate the play head position of a timeline with a timeline. And then we can scrub that timeline with our scroll position.

Don‘t worry if that sounds confusing. We’re going to break it down.

Going “meta”

Let‘s start with an example. We’re going to create a tween that moves some boxes from left to right. Here it is.

Ten boxes that keep going left to right. That’s quite straightforward with Greensock. Here, we use fromTo and repeat to keep the animation going. But, we have a gap at the start of each iteration. We’re also using stagger to space out the movement and that’s something that will play an important role as we continue.

gsap.fromTo('.box', {   xPercent: 100 }, {   xPercent: -200,   stagger: 0.5,   duration: 1,   repeat: -1,   ease: 'none', })

Now comes the fun part. Let’s pause the tween and assign it to a variable. Then let’s create a tween that plays it. We can do this by tweening the totalTime of the tween, which allows us to get or set the tween’s playhead tween, while considering repeats and repeat delays.

const SHIFT = gsap.fromTo('.box', {   xPercent: 100 }, {   paused: true,   xPercent: -200,   stagger: 0.5,   duration: 1,   repeat: -1,   ease: 'none', })  const DURATION = SHIFT.duration()  gsap.to(SHIFT, {   totalTime: DURATION,   repeat: -1,   duration: DURATION,   ease: 'none', })

This is our first “meta” tween. It looks exactly the same but we’re adding another level of control. We can change things on this layer without affecting the original layer. For example, we could change the tween ease to power4.in. This completely changes the animation but without affecting the underlying animation. We’re kinda safeguarding ourselves with a fallback.

Not only that, we might choose to repeat only a certain part of the timeline. We could do that with another fromTo, like this:

The code for that would be something like this.

gsap.fromTo(SHIFT, {   totalTime: 2, }, {   totalTime: DURATION - 1,   repeat: -1,   duration: DURATION,   ease: 'none' })

Do you see where this is going? Watch that tween. Although it keeps looping, the numbers flip on each repeat. But, the boxes are in the correct position.

Achieving the “perfect” loop

If we go back to our original example, there’s a noticeable gap between each repetition.

Here comes the trick. The part that unlocks everything. We need to build a perfect loop.

Let‘s start by repeating the shift three times. It’s equal to using repeat: 3. Notice how we’ve removed repeat: -1 from the tween.

const getShift = () => gsap.fromTo('.box', {   xPercent: 100 }, {   xPercent: -200,   stagger: 0.5,   duration: 1,   ease: 'none', })  const LOOP = gsap.timeline()   .add(getShift())   .add(getShift())   .add(getShift())

We’ve turned the initial tween into a function that returns the tween and we add it to a new timeline three times. And this gives us the following.

OK. But, there’s still a gap. Now we can bring in the position parameter for adding and positioning those tweens. We want it to be seamless. That means inserting each each set of tweens before the previous one ends. That’s a value based on the stagger and the amount of elements.

const stagger = 0.5 // Used in our shifting tween const BOXES = gsap.utils.toArray('.box') const LOOP = gsap.timeline({   repeat: -1 })   .add(getShift(), 0)   .add(getShift(), BOXES.length * stagger)   .add(getShift(), BOXES.length * stagger * 2)

If we update our timeline to repeat and watch it (while adjusting the stagger to see how it affects things)…

You‘ll notice that there‘s a window in the middle there that creates a “seamless” loop. Recall those skills from earlier where we manipulated time? That’s what we need to do here: loop the window of time where the loop is “seamless.”

We could try tweening the totalTime through that window of the loop.

const LOOP = gsap.timeline({   paused: true,   repeat: -1, }) .add(getShift(), 0) .add(getShift(), BOXES.length * stagger) .add(getShift(), BOXES.length * stagger * 2)  gsap.fromTo(LOOP, {   totalTime: 4.75, }, {   totalTime: '+=5',   duration: 10,   ease: 'none',   repeat: -1, }) 

Here, we’re saying tween the totalTime from 4.75 and add the length of a cycle to that. The length of a cycle is 5. And that’s the middle window of the timeline. We can use GSAP’s nifty += to do that, which gives us this:

Take a moment to digest what‘s happening there. This could be the trickiest part to wrap your head around. We’re calculating windows of time in our timeline. It’s kinda hard to visualize but I’ve had a go.

This is a demo of a watch that takes 12 seconds for the hands go round once. It‘s looped infinitely with repeat: -1 and then we‘re using fromTo to animate a specific time window with a given duration. If you, reduce the time window to say 2 and 6, then change the duration to 1, the hands will go from 2 o‘clock to 6 o’clock on repeat. But, we never changed the underlying animation.

Try configuring the values to see how it affects things.

At this point, it’s a good idea to put together a formula for our window position. We could also use a variable for the duration it takes for each box to transition.


Instead of using three stacked timelines, we could loop over our elements three times where we get the benefit of not needing to calculate the positions. Visualizing this as three stacked timelines is a neat way to grok the concept, though, and a nice way to help understand the main idea.

Let’s change our implementation to create one big timeline from the start.

const STAGGER = 0.5 const BOXES = gsap.utils.toArray('.box')  const LOOP = gsap.timeline({   paused: true,   repeat: -1, })  const SHIFTS = [...BOXES, ...BOXES, ...BOXES]  SHIFTS.forEach((BOX, index) => {   LOOP.fromTo(BOX, {     xPercent: 100   }, {     xPercent: -200,     duration: 1,     ease: 'none',   }, index * STAGGER) })

This is easier to put together and gives us the same window. But, we don’t need to think about math. Now we loop through three sets of the boxes and position each animation according to the stagger.

How might that look if we adjust the stagger? It will squish the boxes closer together.

But, it’s broken the window because now the totalTime is out. We need to recalculate the window. Now’s a good time to plug in the formula we calculated earlier.

const DURATION = 1 const CYCLE_DURATION = STAGGER * BOXES.length const START_TIME = CYCLE_DURATION + (DURATION * 0.5) const END_TIME = START_TIME + CYCLE_DURATION  gsap.fromTo(LOOP, {   totalTime: START_TIME, }, {   totalTime: END_TIME,   duration: 10,   ease: 'none',   repeat: -1, })


We could even introduce an “offset” if we wanted to change the starting position.


Now our window starts from a different position.

But still, this isn’t great as it gives us these awkward stacks at each end. To get rid of that effect, we need to think about a “physical” window for our boxes. Or think about how they enter and exit the scene.

We’re going to use document.body as the window for our example. Let’s update the box tweens to be individual timelines where the boxes scale up on enter and down on exit. We can use yoyo and repeat: 1 to achieve entering and exiting.

SHIFTS.forEach((BOX, index) => {   const BOX_TL = gsap     .timeline()     .fromTo(       BOX,       {         xPercent: 100,       },       {         xPercent: -200,         duration: 1,         ease: 'none',       }, 0     )     .fromTo(       BOX,       {         scale: 0,       },       {         scale: 1,         repeat: 1,         yoyo: true,         ease: 'none',         duration: 0.5,       },       0     )   LOOP.add(BOX_TL, index * STAGGER) })

Why are we using a timeline duration of 1? It makes things easier to follow. We know the time is 0.5 when the box is at the midpoint. It‘s worth noting that easing won’t have the effect we usually think of here. In fact, easing will actually play a part in how the boxes position themselves. For example, an ease-in would bunch the boxes up on the right before they move across.

The code above gives us this.

Almost. But, our boxes disappear for a time in the middle. To fix this, let’s introduce the immediateRender property. It acts like animation-fill-mode: none in CSS. We’re telling GSAP that we don’t want to retain or pre-record any styles that are being set on a box.

SHIFTS.forEach((BOX, index) => {   const BOX_TL = gsap     .timeline()     .fromTo(       BOX,       {         xPercent: 100,       },       {         xPercent: -200,         duration: 1,         ease: 'none',         immediateRender: false,       }, 0     )     .fromTo(       BOX,       {         scale: 0,       },       {         scale: 1,         repeat: 1,         zIndex: BOXES.length + 1,         yoyo: true,         ease: 'none',         duration: 0.5,         immediateRender: false,       },       0     )   LOOP.add(BOX_TL, index * STAGGER) })

That small change fixes things for us! Note how we’ve also included z-index: BOXES.length. That should safeguard us against any z-index issues.

There we have it! Our first infinite seamless loop. No duplicate elements and perfect continuation. We’re bending time! Pat yourself on the back if you’ve gotten this far! 🎉

If we want to see more boxes at once, we can tinker with the timing, stagger, and ease. Here, we have a STAGGER of 0.2 and we’ve also introduced opacity into the mix.

The key part here is that we can make use of repeatDelay so that the opacity transition is quicker than the scale. Fade in over 0.25 seconds. Wait 0.5 seconds. Fade back out over 0.25 seconds.

.fromTo(   BOX, {     opacity: 0,   }, {     opacity: 1,     duration: 0.25,     repeat: 1,     repeatDelay: 0.5,     immediateRender: false,     ease: 'none',     yoyo: true,   }, 0)

Cool! We could do whatever we want with those in and out transitions. The main thing here is that we have our window of time that gives us the infinite loop.

Hooking this up to scroll

Now that we have a seamless loop, let’s attach it to scroll. For this we can use GSAP’s ScrollTrigger. This requires an extra tween to scrub our looping window. Note how we’ve set the loop to be paused now, too.

const LOOP_HEAD = gsap.fromTo(LOOP, {   totalTime: START_TIME, }, {   totalTime: END_TIME,   duration: 10,   ease: 'none',   repeat: -1,   paused: true, })  const SCRUB = gsap.to(LOOP_HEAD, {   totalTime: 0,   paused: true,   duration: 1,   ease: 'none', })

The trick here is to use ScrollTrigger to scrub the play head of the loop by updating the totalTime of SCRUB. There are various ways we could set up this scroll. We could have it horizontal or bound to a container. But, what we‘re going to do is wrap our boxes in a .boxes element and pin that to the viewport. (This fixes its position in the viewport.) We’ll also stick with vertical scrolling. Check the demo to see the styling for .boxes which sets things to the size of the viewport.

import ScrollTrigger from 'https://cdn.skypack.dev/gsap/ScrollTrigger' gsap.registerPlugin(ScrollTrigger)  ScrollTrigger.create({   start: 0,   end: '+=2000',   horizontal: false,   pin: '.boxes',   onUpdate: self => {     SCRUB.vars.totalTime = LOOP_HEAD.duration() * self.progress     SCRUB.invalidate().restart()   } })

The important part is inside onUpdate. That’s where we set the totalTime of the tween based on the scroll progress. The invalidate call flushes any internally recorded positions for the scrub. The restart then sets the position to the new totalTime we set.

Try it out! We can go back and forth in the timeline and update the position.

How cool is that? We can scroll to scrub a timeline that scrubs a timeline that is a window of a timeline. Digest that for a second because that‘s what’s happening here.

Time travel for infinite scrolling

Up to now, we‘ve been manipulating time. Now we’re going to time travel!

To do this, we‘re going to use some other GSAP utilities and we‘re no longer going to scrub the totalTime of LOOP_HEAD. Instead, we’re going to update it via proxy. This is another great example of going “meta” GSAP.

Let’s start with a proxy object that marks the playhead position.

const PLAYHEAD = { position: 0 }

Now we can update our SCRUB to update the position. At the same time, we can use GSAP’s wrap utility, which wraps the position value around the LOOP_HEAD duration. For example, if the duration is 10 and we provide the value 11, we will get back 1.

const POSITION_WRAP = gsap.utils.wrap(0, LOOP_HEAD.duration())  const SCRUB = gsap.to(PLAYHEAD, {   position: 0,   onUpdate: () => {     LOOP_HEAD.totalTime(POSITION_WRAP(PLAYHEAD.position))   },   paused: true,   duration: 1,   ease: 'none', })

Last, but not least, we need to revise ScrollTrigger so it updates the correct variable on the SCRUB. That’s position, instead of totalTime.

ScrollTrigger.create({   start: 0,   end: '+=2000',   horizontal: false,   pin: '.boxes',   onUpdate: self => {     SCRUB.vars.position = LOOP_HEAD.duration() * self.progress     SCRUB.invalidate().restart()   } })

At this point we’ve switched to a proxy and we won’t see any changes.

We want an infinite loop when we scroll. Our first thought might be to scroll to the start when we complete scroll progress. And it would do exactly that, scroll back. Although that‘s what we want to do, we don’t want the playhead to scrub backwards. This is where totalTime comes in. Remember? It gets or sets the position of the playhead according to the totalDuration which includes any repeats and repeat delays.

For example, say the duration of the loop head was 5 and we got there, we won‘t scrub back to 0. Instead, we will keep scrubbing the loop head to 10. If we keep going, it‘ll go to 15, and so on. Meanwhile, we‘ll keep track of an iteration variable because that tells us where we are in the scrub. We’ll also make sure that we only update iteration when we hit the progress thresholds.

Let’s start with an iteration variable:

let iteration = 0

Now let’s update our ScrollTrigger implementation:

const TRIGGER = ScrollTrigger.create({   start: 0,   end: '+=2000',   horizontal: false,   pin: '.boxes',   onUpdate: self => {     const SCROLL = self.scroll()     if (SCROLL > self.end - 1) {       // Go forwards in time       WRAP(1, 1)     } else if (SCROLL < 1 && self.direction <; 0) {       // Go backwards in time       WRAP(-1, self.end - 1)     } else {       SCRUB.vars.position = (iteration + self.progress) * LOOP_HEAD.duration()       SCRUB.invalidate().restart()      }   } }) 

Notice how we‘re now factoring iteration into the position calculation. Remember that this gets wrapped with the scrubber. We‘re also detecting when we hit the limits of our scroll, and that’s the point where we WRAP. This function sets the appropriate iteration value and sets the new scroll position.

const WRAP = (iterationDelta, scrollTo) => {   iteration += iterationDelta   TRIGGER.scroll(scrollTo)   TRIGGER.update() }

We have infinite scrolling! If you have one of those fancy mice with the scroll wheel that you can let loose on, give it a go! It’s fun!

Here’s a demo that displays the current iteration and progress:

Scroll snapping

We‘re there. But, there are always ”nice to haves” when working on a feature like this. Let’s start with scroll snapping. GSAP makes this easy, as we can use gsap.utils.snap without any other dependencies. That handles snapping to a time when we provide the points. We declare the step between 0 and 1 and we have 10 boxes in our example. That means a snap of 0.1 would work for us.

const SNAP = gsap.utils.snap(1 / BOXES.length)

And that returns a function we can use to snap our position value.

We only want to snap once the scroll has ended. For that, we can use an event listener on ScrollTrigger. When the scroll ends, we are going to scroll to a certain position.

ScrollTrigger.addEventListener('scrollEnd', () => {   scrollToPosition(SCRUB.vars.position) })

And here’s scrollToPosition:

const scrollToPosition = position => {   const SNAP_POS = SNAP(position)   const PROGRESS =     (SNAP_POS - LOOP_HEAD.duration() * iteration) / LOOP_HEAD.duration()   const SCROLL = progressToScroll(PROGRESS)   TRIGGER.scroll(SCROLL) }

What are we doing here?

  1. Calculating the point in time to snap to
  2. Calculating the current progress. Let’s say the LOOP_HEAD.duration() is 1 and we’ve snapped to 2.5. That gives us a progress of 0.5 resulting in an iteration of 2, where 2.5 - 1 * 2 / 1 === 0.5 . We calculate the progress so that it’s always between 1 and 0.
  3. Calculating the scroll destination. This is a fraction of the distance our ScrollTrigger can cover. In our example, we’ve set a distance of 2000 and we want a fraction of that. We create a new function progressToScroll to calculate it.
const progressToScroll = progress =>   gsap.utils.clamp(1, TRIGGER.end - 1, gsap.utils.wrap(0, 1, progress) * TRIGGER.end)

This function takes the progress value and maps it to the largest scroll distance. But we use a clamp to make sure the value can never be 0 or 2000. This is important. We are safeguarding against snapping to these values as it would put us in an infinite loop.

There is a bit to take in there. Check out this demo that shows the updated values on each snap.

Why are things a lot snappier? The scrubbing duration and ease have been altered. A smaller duration and punchier ease give us the snap.

const SCRUB = gsap.to(PLAYHEAD, {   position: 0,   onUpdate: () => {     LOOP_HEAD.totalTime(POSITION_WRAP(PLAYHEAD.position))   },   paused: true,   duration: 0.25,   ease: 'power3', })

But, if you played with that demo, you‘ll notice there‘s an issue. Sometimes when we wrap around inside the snap, the playhead jumps about. We need to account for that by making sure we wrap when we snap — but, only when it’s necessary.

const scrollToPosition = position => {   const SNAP_POS = SNAP(position)   const PROGRESS =     (SNAP_POS - LOOP_HEAD.duration() * iteration) / LOOP_HEAD.duration()   const SCROLL = progressToScroll(PROGRESS)   if (PROGRESS >= 1 || PROGRESS < 0) return WRAP(Math.floor(PROGRESS), SCROLL)   TRIGGER.scroll(SCROLL) }

And now we have infinite scrolling with snapping!

What next?

We’ve completed the groundwork for a solid infinite scroller. We can leverage that to add things, like controls or keyboard functionality. For example, this could be a way to hook up “Next” and “Previous” buttons and keyboard controls. All we have to do is manipulate time, right?

const NEXT = () => scrollToPosition(SCRUB.vars.position - (1 / BOXES.length)) const PREV = () => scrollToPosition(SCRUB.vars.position + (1 / BOXES.length))  // Left and Right arrow plus A and D document.addEventListener('keydown', event => {   if (event.keyCode === 37 || event.keyCode === 65) NEXT()   if (event.keyCode === 39 || event.keyCode === 68) PREV() })  document.querySelector('.next').addEventListener('click', NEXT) document.querySelector('.prev').addEventListener('click', PREV)

That could give us something like this.

We can leverage our scrollToPosition function and bump the value as we need.

That’s it!

See that? GSAP can animate more than elements! Here, we bent and manipulated time to create an almost perfect infinite slider. No duplicate elements, no mess, and good flexibility.

Let’s recap what we covered:

  • We can animate an animation. 🤯
  • We can think about timing as a positioning tools when we manipulate time.
  • How to use ScrollTrigger to scrub an animation via proxy.
  • How to use some of GSAP’s awesome utilities to handle logic for us.

You can now manipulate time! 😅

That concept of going “meta” GSAP opens up a variety of possibilities. What else could you animate? Audio? Video? As for the ”Cover Flow” demo, here’s where that went!

The post Going “Meta GSAP”: The Quest for “Perfect” Infinite Scrolling appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , , ,

WordPress.com: One CMS, Infinite Possibilities

(This is a sponsored post.)

Have you ever looked at a site and knew exactly what CMS powers it? You might see a distinctive design aesthetic that gives it away. Or maybe it’s something even less obvious and even harder to articulate, but you know it when you see it.

That seems true with just about any platform, especially those that rely on a set of templates. If you were to jump from one site ot another on the same platform, you can see the similarities, sort of like walking down the street of a neighborhood where all the homes are designed by the same architect.

It’s not a bad thing. But like homes, we tend to want websites with personality and that feel unique. That’s one of the things that makes WordPress.com a nice hosted platform option.

Yes, it has core themes, some of which are commonly used. What it also has is hundreds of others, including 110 themes that are free. The designs range from portfolio- and business-themed sites to ones themed around traditional blogs, weddings, travel, music, and food. There’s so many to choose from, and they’re introducing more every year. Take a look through some live sites using WordPress.com. The variety is awesome and showcases the many possibilities of WordPress as a content management system.

We’ve said it before: if you can build a site with WordPress.com, you should build a site on WordPress.com. We’re proud to have WordPress as a sponsor here at CSS-Tricks and wouldn’t hesitate to recommend it to anyone who needs a quick and easy way to spin up a site. Plus, with a free plan tier, it’s even easier to get started.

Start your website

Direct Link to ArticlePermalink

The post WordPress.com: One CMS, Infinite Possibilities appeared first on CSS-Tricks.


, ,