Remix.run like Homepage Animation

What's it about?

Learn how to create homepage animations like remix.run, where the content scrolls along (and updates) with user's scroll.

2 min to read
song
beginner
html
css
typescript
svelte
react
animation
learning

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.

Container (Svelte)
<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>

Using Container (Svelte)
<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 (tap) and check out some other stuff I wrote below ๐Ÿ‘‡๐Ÿป