Remix.run like Homepage Animation
Learn how to create homepage animations like remix.run, where the content scrolls along (and updates) with user's scroll.
Tags
Final Result ๐
Just scrolling up and down going on here ๐๐ป
TLDR ๐ฅฑ!!!
All you need is this ๐ :-
// bcr = boundingClientRect, shortened for brevity const bcr = containerDivElement.getBoundingClientRect(); const counter = 1 - (bcr.bottom / bcr.height);
Like seriously, if you calculate a new counter
every time the user scrolls, you can use it to create magical animations.
This counter
value goes from 0
to 1
as the user scrolls from the top to the bottom of the container.
But How Does it Work? ๐ค
Sing along with me ๐ถ,
๐คฉ I came across this animation on remix.run website,
โ๐ป It looked so cool, I turned my pc on and soon started to write,
๐ฉ The sh*t code and the good code, I tried coding it all,
๐คซ Eventually I found the secret that Iโm about tell you all,
๐ฆ We kick off with a container <div />
, and hereโs the code for that,
๐ Iโm using Svelte here, but sure you can use React ,
// for React, use `useRef` hook, <div ref={containerRef} /> <div bind:this={containerRef} style="height: 400vh;"> <slot {containerVisible} {counter} /> </div>
๐ข This little snippet creates a <div />
thatโs super super tall (400vh),
๐ฐ And your content goes inside of it, thatโs why we used a <slot />
๐ Now, we just need to calculate the counter
when user scrolls,
๐ฅ And hereโs the code to do that, just gimme a drum roll,
// same as useLayoutEffect for React afterUpdate(() => { // the fu#k's this ๐๐ป, right? ๐ค if (!containerVisible) return; const bcr = containerRef.getBoundingClientRect(); const ratio = bcr.bottom / bcr.height; const counter = 1 - ( ratio < 0 ? 0 : ratio > 1 ? 1 : ratio ); });
โ๐ป But Wait, Whatโs the deal with that containerVisible
in there?
๐คญ Itโs just a basic bool, updates when containerโs in viewport,
๐ Itโs true
when you see it, ๐ and false
when you donโt,
๐งฉ And we use Intersection Observer API for doing that,
โ๏ธ And hereโs the code for it, should also works in the React,
// use a `useState` hook for React let intersectionObserver: IntersectionObserver; // same as `useLayoutEffect` for React afterUpdate(() => { intersectionObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { containerVisible = true; } else { containerVisible = false; } }); }); intersectionObserver.observe(containerRef); }); // same as `useEffect`'s cleanup for React onDestroy(() => { intersectionObserver && intersectionObserver.disconnect(); });
๐ But wait, thereโs one more thing, a vital part indeed,
๐๐ปโโ๏ธ A re-render trigger, to fulfill our animation need,
๐ As the use scrolls down, we have to force a re-render,
๐ฆด Letโs add a state variable, weโll call it justToRerender
,
๐ Bind onScroll
to the <body />
, let animation dwell,
๐๐ป It updates as user scrollโs, through heaven or through hell,
// this is just a `useState` for React let justToRerender: number = 0; // for React, use `useEffect` along with `document.addEventListener` <svelte:window on:scroll={() => { justToRerender = window.scrollY; }} />
๐ Now whenever the user scrolls, the justToRerender
changes,
๐ฎโ๐จ And donโt sweat it, use this trick, there are no dangers,
โ Now counter goes through 0 to 1 as the user scrolls down,
๐ We use this to create animations, thatโs how we win the crown,
Letโs Animate ๐๐ป
๐ Itโs simple, super simple actually, now that we have that counter,
๐จ We can use it to animate anything, and I mean anything we encounter,
๐ญ Hereโs a simple example, letโs animate the scale & opacity,
๐ญ Like a painter with a brush, crafting with ferocity,
<Container let:counter let:containerVisible > {#if containerVisible} <div style=" position: sticky; top: 50vh; opacity: {counter}; transform: scale({counter + 0.3}) " > C๐ฝrnHub </div> {/if} </Container>
๐ And thatโs it, โฆ weโre finally done,
๐ And now you can also create animations that are super duper fun.
Full Working Code (Svelte) ๐
Iโve used TailwindCSS for styling, and Iโve not removed it here from the code, so that if you copy-paste this, itโll look exactly like the final result.
<script lang="ts"> import { afterUpdate, onDestroy } from "svelte"; import type { HTMLAttributes } from "svelte/elements"; interface Props extends HTMLAttributes<HTMLDivElement> {} const { class: svelteClass, ...props } = $$restProps; let containerRef: HTMLDivElement; let justToRerender: number = 0; let intersectionObserver: IntersectionObserver; export let containerVisible: boolean = false; // gradually goes from 0 to 1 as user scrolls down // might not work as expected, if the container is the bottom most element in the page // in this case, use an offset value to adjust the counter export let counter: number = 0; // calculate counter afterUpdate(() => { if (!containerVisible) return; const ratio = containerRef.getBoundingClientRect().bottom / containerRef.getBoundingClientRect().height; counter = 1 - (ratio < 0 ? 0 : ratio > 1 ? 1 : ratio); }); // Intersection Observer API afterUpdate(() => { intersectionObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { containerVisible = true; } else { containerVisible = false; } }); }); intersectionObserver.observe(containerRef); }); onDestroy(() => { intersectionObserver && intersectionObserver.disconnect(); }); </script> <svelte:window on:scroll={() => { justToRerender = window.scrollY; }} /> <div class={`w-full h-[400vh] ${svelteClass}`} {...props} bind:this={containerRef} > <slot {containerVisible} {counter} /> </div>
<script lang="ts"> import Container from "./container.svelte"; </script> <Container let:counter let:containerVisible class="flex justify-center items-center bg-gradient-to-b from-gray-950 via-amber-900 to-gray-950" > {#if containerVisible} <div class="sticky top-[45vh] py-20 w-96 aspect-square flex justify-center bg-black shadow-2xl shadow-amber-600 rounded-lg" style=" opacity: {counter}; transform: scale({counter + 0.3}) " > C๐ฝrnHub </div> {/if} </Container>
Full Working Code (React) ๐
To be updated soonโฆ
Thanks ๐ค for reading all the way till down here. Please consider subscribing(click) (tap) and check out some other stuff I wrote below ๐๐ป