<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Giles Perry]]></title><description><![CDATA[Giles Perry]]></description><link>https://blog.gilesperry.info</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 23:12:23 GMT</lastBuildDate><atom:link href="https://blog.gilesperry.info/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Advanced Scroll Effects in Framer]]></title><description><![CDATA[Three years ago I wrote an article for Framer on how to create scroll interaction effects. Much has happened since then, in particular the ability to publish fully functioning websites.
So how do the techniques I explored fit this new world?
We now h...]]></description><link>https://blog.gilesperry.info/advanced-scroll-effects-in-framer</link><guid isPermaLink="true">https://blog.gilesperry.info/advanced-scroll-effects-in-framer</guid><category><![CDATA[React]]></category><category><![CDATA[framer]]></category><dc:creator><![CDATA[Giles Perry]]></dc:creator><pubDate>Tue, 26 Jul 2022 15:00:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1658011844816/-wflfL5GQ.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Three years ago I wrote an article for <a target="_blank" href="https://www.framer.com?via=giles_perry">Framer</a> on how to create scroll interaction effects. Much has happened since then, in particular the ability to publish fully functioning websites.</p>
<p>So how do the techniques I explored fit this new world?</p>
<p>We now have two contexts where you might want to apply scrolling effects: scrolling of the whole page relative to the viewport, and scrolling of content that overflows its container. I will concentrate on page scrolling, but the techniques for element scrolling are the same, so look out for a pointer on this.</p>
<p>My focus will be on code, but many of the effects I first wrote about are now more easily applied through native features. I will speak about these options too.</p>
</blockquote>
<h3 id="heading-in-this-article-you-will-learn-how-to">In this article you will learn how to...</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-parallax">Create parallax</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-more-scroll-driven-effects">Use scrolling to drive the transformation of another layer</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-now-is-a-good-time-to-talk-about-element-scrolling">Handle element scrolling</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-dynamic-nav-bar">Build a dynamic nav bar</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-triggering-animations-at-set-scroll-positions">Trigger animations at set scroll positions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-viewport-awareness">Apply effects while an element is in view</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-scroll-away-nav-bar">Build a scroll-away nav bar</a></p>
</li>
</ul>
<hr />
<h1 id="heading-first-lets-talk-theory">First let’s talk theory</h1>
<p>Behind the scenes, the things you create in Framer are powered by HTML, CSS and JavaScript. Framer is based on React, a JavaScript library for building user interfaces. Everything you make in Framer is a <a target="_blank" href="https://reactjs.org/docs/components-and-props.html">React component</a>.</p>
<p>Components have properties such as height, width, or scale. We call them <code>props</code>. When you add a frame to your project and pick a background colour in the Properties panel, you are setting the frame’s <code>props</code>.</p>
<p>Framer lets you apply code to a layer that will override the layer’s props when it is displayed in the <em>Preview</em> window, and on your published site. <a target="_blank" href="https://www.framer.com/developers/guides/overrides/">Code Overrides</a> are incredibly powerful, so it's worth getting to know them. Through overrides you have access to pretty much anything the web is capable of.</p>
<h2 id="heading-well-start-with-something-simple">We'll start with something simple</h2>
<p>Let's say you want to override a layer's height.</p>
<p>Height is a CSS style attribute, so to make the layer 100 pixels high we need to update its <code>style</code> prop using the value <code>{ height: 100 }</code>.</p>
<p>Here’s the <strong>Code Override</strong> to do exactly that:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ComponentType } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withSpecificHeight</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> &lt;Component {...props} style={{ ...props.style, height: <span class="hljs-number">100</span> }} /&gt;
    }
}
</code></pre>
<p>If you are new to coding, being able to read and understand even a simple example like this means taking on JavaScript, TypeScript and React all at once.</p>
<h2 id="heading-lets-pick-it-apart">Let's pick it apart</h2>
<p>A React component is simply a function that returns a block of HTML.</p>
<p>Overrides are a little bit more complicated. In React, an override is what is known as a <a target="_blank" href="https://reactjs.org/docs/higher-order-components.html">higher-order component</a>. It’s <strong>a function that takes another React component</strong> as an input <strong>and returns a new component</strong>. The override uses this technique to modify the original component’s props.</p>
<p>So, when you add an override to a layer, you are replacing the underlying React component with one that has been modified. By convention, override functions have “with” as a prefix because they return a component <em>with</em> a modification.</p>
<p>Let's replay our example one step at a time:</p>
<pre><code class="lang-plaintext">import type { ComponentType } from "react"
</code></pre>
<p>Import a type definition, <code>ComponentType</code>, provided by React. We'll use this to let the code know that the override function returns a component.</p>
<pre><code class="lang-plaintext">export function withSpecificHeight(Component): ComponentType
</code></pre>
<p>Export a function component called <code>withSpecificHeight</code>. We need to export it so Framer can access it. The function has a single argument: <code>Component</code>. This is the component that Framer will pass into the function.</p>
<p>How about the <code>: ComponentType</code> tacked on the end? That’s TypeScript. It’s a type annotation specifying that this function returns a component.</p>
<p>Everything in the first set of curly braces is the code block that runs when you call the function. It's pretty simple. It just returns the following React function component:</p>
<pre><code class="lang-typescript">(props: <span class="hljs-built_in">any</span>) =&gt; {
    <span class="hljs-keyword">return</span> &lt;Component {...props} style={{ ...props.style, height: <span class="hljs-number">100</span> }} /&gt;
}
</code></pre>
<p>This function defines what will actually be rendered:</p>
<p><code>&lt;Component /&gt;</code> is our original component. Everything else describes the <code>props</code> we want to apply this time round.</p>
<p><code>{...props}</code> is a shorthand syntax for listing out and applying all of the original <code>props</code> intact. Because <code>style</code> is set after this, its value gets overridden:</p>
<pre><code class="lang-plaintext">style={{ ...props.style, height: 100 }}
</code></pre>
<p>Similarly, we want to override <code>height</code> without affecting any other styles: <code>...props.style</code> lists out and applies any existing styles, then we override the <code>height</code>.</p>
<p><strong>Boom! Now you can code. Let's get going!</strong></p>
<blockquote>
<p>All my examples use Overrides. If you are new to Framer you can read more about Overrides in the <a target="_blank" href="https://www.framer.com/developers/guides/overrides/">Framer Developers Guides</a>. If you need an introduction to React, you might enjoy reading the <a target="_blank" href="https://www.framer.com/books/framer-guide-to-react/">Framer Guide to React</a>:</p>
</blockquote>
<hr />
<h1 id="heading-parallax">Parallax</h1>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/parallax-o1ofcv?fontsize=14&amp;hidenavigation=1&amp;hidedevtools=1&amp;theme=dark&amp;view=preview">https://codesandbox.io/embed/parallax-o1ofcv?fontsize=14&amp;hidenavigation=1&amp;hidedevtools=1&amp;theme=dark&amp;view=preview</a></div>
<p> </p>
<p>Parallax is an effect applied to content as it scrolls that creates the illusion of depth by moving background layers more slowly than layers in the foreground. <strong>Parallax is all about relative movement</strong>.</p>
<p>The easy route to parallax is using Framer’s native scroll effects. Speed Effects allow you to define the scrolling speed of any given layer. Read Framer’s <a target="_blank" href="https://www.framer.com/learn/scroll-effects/">documentation</a> to find out how.</p>
<p>This is great. If vertical parallax is all you need you are good to go. For anything more bespoke you'll need an override.</p>
<p>In a lot of cases, the code is surprisingly simple. We want to establish a relationship between the distance scrolled and another value. We have already seen how an override can modify component properties, so we need two more things.</p>
<ol>
<li><p>A way to track the scroll amount.</p>
</li>
<li><p>A way to the transform this valve into the value we want to apply to the layer.</p>
</li>
</ol>
<p>Framer’s animation library, <a target="_blank" href="https://www.framer.com/motion/">Motion</a>, provides utility functions for each of these.</p>
<h2 id="heading-keeping-track-of-the-scroll-distance">Keeping track of the scroll distance</h2>
<p>The first is <a target="_blank" href="https://www.framer.com/docs/use-scroll/">useScroll</a>.</p>
<p>This function returns a value that stays synced to the distance the page is scrolled. Before we can call it in our override function, we need to import it from the library:</p>
<pre><code class="lang-plaintext">import { useScroll } from "framer-motion"
</code></pre>
<p>Now we can get <code>scrollY</code>, the vertical scroll distance in pixels:</p>
<pre><code class="lang-plaintext">const { scrollY } = useScroll()
</code></pre>
<p><code>scrollY</code> is special kind of variable called a <a target="_blank" href="https://www.framer.com/docs/motionvalue/">MotionValue</a> that Framer uses to track the state and velocity of a value that animates or changes rapidly. As you scroll, <code>scrollY</code> will magically update any property we connect to it.</p>
<h2 id="heading-transforming-motionvalues">Transforming MotionValues</h2>
<p>We could apply the value in <code>scrollY</code> directly, but in most cases we need to transform it into another value. For that we need <a target="_blank" href="https://www.framer.com/docs/use-transform/">useTransform</a>.</p>
<p><code>useTransform</code> creates a new MotionValue by transforming the output of a MotionValue we pass to it.</p>
<p>Say we want a layer to drift horizontally as we scroll down the page, translating it 1px left along the x-axis for every 2px we scroll down. i.e. half the scroll speed in a negative x direction.</p>
<p>We can transform <code>scrollY</code> into the <code>x</code> value we need like this:</p>
<pre><code class="lang-plaintext">const x = useTransform(scrollY, (value) =&gt; -value / 2)
</code></pre>
<p>All that's left to do is to use <code>x</code> to translate the layer along the x-axis using the CSS <code>transform</code> property.</p>
<blockquote>
<p><strong>Motion</strong> gives us easy access to these properties via <a target="_blank" href="https://www.framer.com/docs/component/##transform">enhanced style props</a> and <code>x</code>, <code>y</code>, and <code>z</code> Translate shortcuts.</p>
</blockquote>
<p>Here’s the override in its entirety:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ComponentType } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>
<span class="hljs-keyword">import</span> { useScroll, useTransform } <span class="hljs-keyword">from</span> <span class="hljs-string">"framer-motion"</span>

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withParallax</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">const</span> speed = <span class="hljs-number">1</span> / <span class="hljs-number">2</span>
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { scrollY } = useScroll()
        <span class="hljs-keyword">const</span> x = useTransform(scrollY, <span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> -value * speed) <span class="hljs-comment">// scrolling down translates left</span>
        <span class="hljs-keyword">return</span> &lt;Component {...props} style={{ ...props.style, x: x }} /&gt;
    }
}
</code></pre>
<hr />
<h1 id="heading-more-scroll-driven-effects">More scroll driven effects</h1>
<p>The same technique can be used to make scrolling drive all kinds of properties. Here’s another override to add to your collection. Use it to drive a Lottie animation or scrub through a video (both components have a <code>progress</code> prop and can be added from the Insert panel).</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Scrub through a video or drive a Lottie animation by scrolling</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withScrolledProgress</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">const</span> startY = <span class="hljs-number">0</span> <span class="hljs-comment">// scroll position when animation starts</span>
    <span class="hljs-keyword">const</span> distance = <span class="hljs-number">1000</span> <span class="hljs-comment">// scroll distance after which animation ends</span>
    <span class="hljs-keyword">const</span> endY = startY + distance

    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { scrollY } = useScroll()
        <span class="hljs-keyword">const</span> progress = useTransform(scrollY, [startY, endY], [<span class="hljs-number">0</span>, <span class="hljs-number">1</span>])

        <span class="hljs-keyword">return</span> &lt;Component {...props} progress={progress} /&gt;
    }
}
</code></pre>
<p>You will notice we are passing arguments into <code>useTransform</code> in a different format. Instead of a function for transforming the input value, we use arrays to specify input and output ranges. These determine the output required at specific points. Values in between are interpolated.</p>
<hr />
<h1 id="heading-now-is-a-good-time-to-talk-about-element-scrolling">Now is a good time to talk about element scrolling</h1>
<p>The main difference, in the case of element scrolling, is we need a <code>ref</code> to identify the scrolling element.</p>
<p><a target="_blank" href="https://reactjs.org/docs/refs-and-the-dom.html">Refs provide a way for React to access nodes in the DOM</a>.</p>
<p>We also need two overrides:</p>
<ul>
<li><p>One to identify the scroll container.</p>
</li>
<li><p>One to apply the transformed property to the element we want affect.</p>
</li>
</ul>
<p>Let's give it a try... First import a function from React for creating refs:</p>
<pre><code class="lang-plaintext">import { createRef } from "react"
</code></pre>
<p>The <code>ref</code> has to be created outside of the overrides so that both overrides can use the same <code>ref</code>:</p>
<pre><code class="lang-plaintext">const ref = createRef&lt;HTMLDivElement&gt;()
</code></pre>
<p>Here’s the override to attach the <code>ref</code> to a component. Apply this to your scroll container:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withScrollRef</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> &lt;Component {...props} ref={ref} /&gt;
    }
}
</code></pre>
<p>To track the scroll position of a scrollable element, instead of the page, we pass the <code>ref</code> to <code>useScroll</code>'s <code>container</code> option:</p>
<pre><code class="lang-plaintext">const { scrollY } = useScroll({
    container: ref
})
</code></pre>
<p>In all other respects, our parallax override is identical to the one above:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withElementParallax</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">const</span> speed = <span class="hljs-number">1</span> / <span class="hljs-number">2</span>
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { scrollY } = useScroll({
            container: ref
        })
        <span class="hljs-keyword">const</span> x = useTransform(scrollY, <span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> -value * speed) <span class="hljs-comment">// scrolling down translates left</span>
        <span class="hljs-keyword">return</span> &lt;Component {...props} style={{ ...props.style, x: x }} /&gt;
    }
}
</code></pre>
<p>Here’s everything we need in one file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ComponentType } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>
<span class="hljs-keyword">import</span> { createRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>
<span class="hljs-keyword">import</span> { useTransform, useScroll } <span class="hljs-keyword">from</span> <span class="hljs-string">"framer-motion"</span>

<span class="hljs-comment">// create a ref so we can attach it to the scroll container</span>
<span class="hljs-keyword">const</span> ref = createRef&lt;HTMLDivElement&gt;()

<span class="hljs-comment">// apply this to the element being scrolled</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withScrollRef</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> &lt;Component {...props} ref={ref} /&gt;
    }
}

<span class="hljs-comment">// apply this to the element with the scroll effect</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withElementParallax</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">const</span> speed = <span class="hljs-number">1</span> / <span class="hljs-number">2</span>
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { scrollY } = useScroll({
            container: ref
        })
        <span class="hljs-keyword">const</span> x = useTransform(scrollY, <span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> -value * speed) <span class="hljs-comment">// scrolling down translates left</span>
        <span class="hljs-keyword">return</span> &lt;Component {...props} style={{ ...props.style, x: x }} /&gt;
    }
}
</code></pre>
<p>The remaining examples will stick to viewport scrolling, but you can adapt them to element scrolling using this framework.</p>
<hr />
<h1 id="heading-dynamic-nav-bar">Dynamic nav bar</h1>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/dynamic-nav-bar-mhsoy7?fontsize=14&amp;hidenavigation=1&amp;hidedevtools=1&amp;theme=dark&amp;view=preview">https://codesandbox.io/embed/dynamic-nav-bar-mhsoy7?fontsize=14&amp;hidenavigation=1&amp;hidedevtools=1&amp;theme=dark&amp;view=preview</a></div>
<p> </p>
<p>A nav bar that collapses when you scroll is another great use case for scroll driven transformations. The override we need is very similar to <code>withScrolledProgress</code>.</p>
<p>By default, output from <code>useTransform</code> is clamped to stay within the given range. Using it this way, as we have so far, would allow us to scroll between a maximum and minimum height.</p>
<p>However, apps often have a bar that shrinks to a minimum size when the user scrolls up, but stretches as far as you are able to pull it in the other direction. If that's the case, we only want to clamp the value in one direction. The trick to making that work is unclamping the transform while repeating the final input and output values.</p>
<p>Here’s what that looks like:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withDynamicNavBar</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-comment">// Value being driven by scrolling (e.g. height)</span>
    <span class="hljs-keyword">const</span> initialValue = <span class="hljs-number">140</span>
    <span class="hljs-keyword">const</span> finalValue = <span class="hljs-number">88</span>

    <span class="hljs-keyword">const</span> speed = <span class="hljs-number">1</span>
    <span class="hljs-keyword">const</span> scrollDistance = (initialValue - finalValue) / speed

    <span class="hljs-keyword">const</span> startY = <span class="hljs-number">0</span> <span class="hljs-comment">// scroll position when transition starts</span>
    <span class="hljs-keyword">const</span> endY = startY + scrollDistance

    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { scrollY } = useScroll()
        <span class="hljs-keyword">const</span> scrollOutput = useTransform(
            scrollY,
            [startY, endY, endY],
            [initialValue, finalValue, finalValue],
            {
                clamp: <span class="hljs-literal">false</span>,
            }
        )
        <span class="hljs-keyword">return</span> &lt;Component {...props} style={{ ...props.style, height: scrollOutput }} /&gt;
    }
}
</code></pre>
<hr />
<h1 id="heading-triggering-animations-at-set-scroll-positions">Triggering animations at set scroll positions</h1>
<p>Another feature we see in dynamic navigation bars is a title, which fades in when the bar reaches its minimum size. <strong>How can we animate the opacity of the title</strong> so it appears only when <code>scrollY</code> passes a threshold?</p>
<h2 id="heading-its-time-for-a-bit-more-theory">It's time for a bit more theory…</h2>
<p>When you imaging coding an interaction, it feels like you need code telling the application exactly what to do:</p>
<blockquote>
<p>Change the opacity of my title to a new value when I scroll past a threshold.</p>
</blockquote>
<p>Code like this is <em>imperative</em>.</p>
<p>React requires a new way of thinking. Instead of specifying what to do, the code is <em>declarative</em>; it tells the application how to be:</p>
<blockquote>
<p>Be transparent below the threshold, and be opaque above the threshold.</p>
</blockquote>
<p>React takes care of everything else, making sure the opacity changes when the condition changes.</p>
<p>To make this happen our code needs three parts:</p>
<ul>
<li><p>A variable to keep track of the state of our application: is <code>scrollY</code> past a limit, <code>true</code> or <code>false</code>?</p>
</li>
<li><p>Something to listen for changes to <code>scrollY</code> and update the application state if the limit is passed.</p>
</li>
<li><p>A function telling the title to animate to either 1 or 0 opacity depending the current state.</p>
</li>
</ul>
<p>We are going to need a couple of <a target="_blank" href="https://reactjs.org/docs/hooks-intro.html">React Hooks</a> to get this done. Let’s import them:</p>
<pre><code class="lang-plaintext">import { useState, useEffect } from "react"
</code></pre>
<h2 id="heading-adding-a-state-variable">Adding a state variable</h2>
<p>We need a Boolean (<code>true</code> or <code>false</code>) state variable where we will keep a record of whether the scroll distance <strong>is past</strong> the <strong>threshold</strong> or not. The <code>useState</code> Hook gives us a way to create and update a state variable:</p>
<pre><code class="lang-plaintext">const [isPastThreshold, setIsPastThreshold] = useState(false)
</code></pre>
<p><code>isPastThreshold</code> is the state variable and <code>setIsPastThreshold</code> is the setter function that allows us to update it. The initial state is <code>false</code>.</p>
<h2 id="heading-listening-for-changes">Listening for changes</h2>
<p>Listeners can be added to <code>MotionValue</code>s with the <code>onChange</code> method. This method passes the latest value into a function we provide every time the <code>MotionValue</code> changes.</p>
<pre><code class="lang-plaintext">scrollY.onChange((latest) =&gt; setIsPastThreshold(latest &gt; thresholdY))
</code></pre>
<p>We are required by React to add the listener <strong>after the component is rendered</strong>. Long story short to run some code after rendering we need to add an <em>Effect</em>. That’s where <code>useEffect</code> comes in:</p>
<pre><code class="lang-typescript">useEffect(
    <span class="hljs-function">() =&gt;</span> scrollY.onChange(<span class="hljs-function">(<span class="hljs-params">latest</span>) =&gt;</span> setIsPastThreshold(latest &gt; thresholdY)),
    []
)
</code></pre>
<h2 id="heading-telling-the-component-how-to-be">Telling the component how to be</h2>
<p>This is as simple as setting opacity on the component's <code>animate</code> prop using a conditional value.</p>
<pre><code class="lang-plaintext">animate={{ opacity: isPastThreshold ? 1 : 0 }}
</code></pre>
<p>When <code>isPastThreshold</code> is <code>true</code> the opacity value will animate to 1, otherwise it will animate to 0.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withScrollToggledState</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">const</span> thresholdY = <span class="hljs-number">100</span> <span class="hljs-comment">// set the scroll position where you want the state change</span>
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { scrollY } = useScroll()
        <span class="hljs-keyword">const</span> [isPastThreshold, setIsPastThreshold] = useState(<span class="hljs-literal">false</span>)
        useEffect(
            <span class="hljs-function">() =&gt;</span>
                scrollY.onChange(<span class="hljs-function">(<span class="hljs-params">latest</span>) =&gt;</span>
                    setIsPastThreshold(latest &gt; thresholdY)
                ),
            []
        )
        <span class="hljs-keyword">return</span> (
            &lt;Component
                {...props}
                animate={{ opacity: isPastThreshold ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span> }}
            /&gt;
        )
    }
}
</code></pre>
<p>Get used to seeing this:</p>
<p><code>const property = condition ? valueWhenTrue : valueWhenFalse</code></p>
<p>In React, this is your best friend…</p>
<h2 id="heading-triggering-a-variant-switch">Triggering a variant switch</h2>
<p>Now we know how to trigger state changes, we can use the same trick on the <code>variant</code> prop:</p>
<pre><code class="lang-plaintext">variant={isPastThreshold ? "Second" : "First"}
</code></pre>
<p>When you try this for the first time, you see the power of declarative code in sharp focus. Set one simple condition and Magic Motion will handle everything.</p>
<blockquote>
<p>This is another scenario that can be handled natively. You’ll find the Scroll Variant option when you add an Effect to any component with more than one variant. Read Framer’s documentation to learn about native <a target="_blank" href="https://www.framer.com/learn/scroll-effects/">Scroll Effects</a>.</p>
</blockquote>
<hr />
<h1 id="heading-viewport-awareness">Viewport awareness</h1>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/scale-b30sjs?fontsize=14&amp;hidenavigation=1&amp;hidedevtools=1&amp;theme=dark&amp;view=preview">https://codesandbox.io/embed/scale-b30sjs?fontsize=14&amp;hidenavigation=1&amp;hidedevtools=1&amp;theme=dark&amp;view=preview</a></div>
<p> </p>
<p>So far we have controlled when effects are applied by hard-coding the scroll position. This approach can be limiting. If we want the same effect on a number of layers, we need a bespoke override for each. If we change the element's position, we need to update our code. Even worse, if our layout is responsive, we may not know element positions upfront.</p>
<p>Luckily, Motion has our back.</p>
<h2 id="heading-animate-while-in-view">Animate while in view</h2>
<p>Triggering animations as an element enters the viewport is a simple as setting Motion's <code>whileInView</code> prop.</p>
<p>Add the <code>viewport</code> prop if you need more control over how the viewport is detected.</p>
<p>Here's an override that animates an element's <code>opacity</code> the first time the element appears in the viewport:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withAnimateWhileInView</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> (
            &lt;Component
                {...props}
                initial={{ opacity: <span class="hljs-number">0</span> }}
                whileInView={{ opacity: <span class="hljs-number">1</span> }}
                viewport={{ once: <span class="hljs-literal">true</span> }}
            /&gt;
        )
    }
}
</code></pre>
<blockquote>
<p>You can also use <code>whileInView</code> for animating between variants. <a target="_blank" href="https://www.framer.com/docs/scroll-animations/#scroll-triggered-animations">Take a look at the docs</a> to learn more about <code>whileInView</code> and its corresponding <code>viewport</code> options.</p>
</blockquote>
<h2 id="heading-how-about-scroll-driven-animations">How about scroll-driven animations?</h2>
<p>You can <a target="_blank" href="https://www.framer.com/docs/use-scroll/##element-position">track an element's progress within the viewport</a> by passing its <code>ref</code> to <code>useScroll</code>'s <code>target</code> option.</p>
<p>Let's see if we can reproduce the scaling effect shown in the demo above.</p>
<p>If we define the <code>start</code> and <code>end</code> of the viewport from top to bottom, and apply the same logic to the circles, then each circle should:</p>
<ul>
<li><p>start animating when its <code>end</code> intersects with the viewport's <code>end</code>,</p>
</li>
<li><p>and finish when its <code>start</code> intersects with the viewport's <code>start</code>.</p>
</li>
</ul>
<p>In other words we want make each circle a scroll <code>target</code>, and measure its <strong>progress</strong> as it scrolls from <code>"end end"</code> to <code>"start start"</code>.</p>
<p>Here is an override you can attach to each circle to make that happen:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ComponentType } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>
<span class="hljs-keyword">import</span> { useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>
<span class="hljs-keyword">import</span> { useScroll, useTransform } <span class="hljs-keyword">from</span> <span class="hljs-string">"framer-motion"</span>

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withScrollLinkedScale</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> ref = useRef(<span class="hljs-literal">null</span>)

        <span class="hljs-keyword">const</span> { scrollYProgress } = useScroll({
            target: ref,
            offset: [<span class="hljs-string">"end end"</span>, <span class="hljs-string">"start start"</span>],
        })

        <span class="hljs-keyword">const</span> scale = useTransform(scrollYProgress, [<span class="hljs-number">0</span>, <span class="hljs-number">0.5</span>, <span class="hljs-number">1</span>], [<span class="hljs-number">0.5</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0.5</span>])

        <span class="hljs-keyword">return</span> (
            &lt;Component {...props} ref={ref} style={{ ...props.style, scale }} /&gt;
        )
    }
}
</code></pre>
<p><strong>There are a few things to call out:</strong></p>
<p>We met <code>ref</code>s earlier. This time we don't need to share the <code>ref</code> so we create it inside the override with React's <code>useRef</code> hook.</p>
<pre><code class="lang-plaintext">const ref = useRef(null)
</code></pre>
<p>Instead of the absolute scroll position, in pixels, we tell <code>useScroll</code> to return <code>scrollYProgress</code>: a <code>MotionValue</code> between <code>0</code> and <code>1</code>. Also, we pass in <code>target</code> and <code>offset</code> options.</p>
<pre><code class="lang-plaintext">const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["end end", "start start"],
})
</code></pre>
<p>As <code>scrollYprogress</code> progresses from <code>0</code> when the circle enters the viewport, to <code>0.5</code> in the centre of the viewport, to <code>1</code> at the end, we transform its scale from <code>0.5</code> to <code>1</code> and back to <code>0.5</code>.</p>
<pre><code class="lang-plaintext">const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.5, 1, 0.5])
</code></pre>
<hr />
<h1 id="heading-scroll-away-nav-bar">Scroll-away nav bar</h1>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/scroll-away-9i8k5k?fontsize=14&amp;hidenavigation=1&amp;hidedevtools=1&amp;theme=dark&amp;view=preview">https://codesandbox.io/embed/scroll-away-9i8k5k?fontsize=14&amp;hidenavigation=1&amp;hidedevtools=1&amp;theme=dark&amp;view=preview</a></div>
<p> </p>
<p>I'm going to step up the pace a little and look at a more complex interaction: a commonly seen pattern where a nav bar slides off screen when you scroll down the page and slides back into view when you scroll back.</p>
<p>Thinking declaratively… we need a condition to tell us this: <strong>is</strong> the nav bar <strong>in view</strong> or not?</p>
<p>This will depend on two things:</p>
<ul>
<li><p><strong>is</strong> the user <strong>scrolling back</strong> or not?</p>
</li>
<li><p><strong>is</strong> the page <strong>at</strong> the <strong>top</strong>? (because we always want to show the nav bar when we are at the top of the page)</p>
</li>
</ul>
<p>Which gives us three state variables: <code>isInView</code>, <code>isScrollingBack</code> and <code>isAtTop</code>.</p>
<p>Scroll direction can be calculated by checking if the velocity of <code>scrollY</code> is positive or negative.</p>
<pre><code class="lang-plaintext">const { scrollY } = useScroll()
const scrollVelocity = useVelocity(scrollY)
</code></pre>
<p>We need to listen for changes in scroll velocity in one Effect.</p>
<pre><code class="lang-typescript">useEffect(
    <span class="hljs-function">() =&gt;</span>
        scrollVelocity.onChange(<span class="hljs-function">(<span class="hljs-params">latest</span>) =&gt;</span> {
            <span class="hljs-keyword">if</span> (latest &gt; <span class="hljs-number">0</span>) {
                setIsScrollingBack(<span class="hljs-literal">false</span>)
                <span class="hljs-keyword">return</span>
            }
            <span class="hljs-keyword">if</span> (latest &lt; -threshold) {
                setIsScrollingBack(<span class="hljs-literal">true</span>)
                <span class="hljs-keyword">return</span>
            }
        }),
    []
)
</code></pre>
<p>In another we set <code>isAtTop</code></p>
<pre><code class="lang-plaintext">useEffect(
    () =&gt; scrollY.onChange((latest) =&gt; setIsAtTop(latest &lt;= 0)),
    []
)
</code></pre>
<p>And if either <code>isScrollingBack</code> or <code>isAtTop</code> changes we have a final Effect to update: <code>isInView</code>.</p>
<pre><code class="lang-plaintext">useEffect(
    () =&gt; setIsInView(isScrollingBack || isAtTop),
    [isScrollingBack, isAtTop]
)
</code></pre>
<p>Here's what that all comes to in full:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withScrollAway</span>(<span class="hljs-params">Component</span>): <span class="hljs-title">ComponentType</span> </span>{
    <span class="hljs-keyword">const</span> slideDistance = <span class="hljs-number">100</span> <span class="hljs-comment">// if we are sliding out a nav bar at the top of the screen, this will be it's height</span>
    <span class="hljs-keyword">const</span> threshold = <span class="hljs-number">500</span> <span class="hljs-comment">// only slide it back when scrolling back at velocity above this positive (or zero) value</span>

    <span class="hljs-keyword">return</span> <span class="hljs-function">(<span class="hljs-params">props</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> { scrollY } = useScroll()
        <span class="hljs-keyword">const</span> scrollVelocity = useVelocity(scrollY)

        <span class="hljs-keyword">const</span> [isScrollingBack, setIsScrollingBack] = useState(<span class="hljs-literal">false</span>)
        <span class="hljs-keyword">const</span> [isAtTop, setIsAtTop] = useState(<span class="hljs-literal">true</span>) <span class="hljs-comment">// true if the page is not scrolled or fully scrolled back</span>
        <span class="hljs-keyword">const</span> [isInView, setIsInView] = useState(<span class="hljs-literal">true</span>)

        useEffect(
            <span class="hljs-function">() =&gt;</span>
                scrollVelocity.onChange(<span class="hljs-function">(<span class="hljs-params">latest</span>) =&gt;</span> {
                    <span class="hljs-keyword">if</span> (latest &gt; <span class="hljs-number">0</span>) {
                        setIsScrollingBack(<span class="hljs-literal">false</span>)
                        <span class="hljs-keyword">return</span>
                    }
                    <span class="hljs-keyword">if</span> (latest &lt; -threshold) {
                        setIsScrollingBack(<span class="hljs-literal">true</span>)
                        <span class="hljs-keyword">return</span>
                    }
                }),
            []
        )

        useEffect(
            <span class="hljs-function">() =&gt;</span> scrollY.onChange(<span class="hljs-function">(<span class="hljs-params">latest</span>) =&gt;</span> setIsAtTop(latest &lt;= <span class="hljs-number">0</span>)),
            []
        )

        useEffect(
            <span class="hljs-function">() =&gt;</span> setIsInView(isScrollingBack || isAtTop),
            [isScrollingBack, isAtTop]
        )

        <span class="hljs-keyword">return</span> (
            &lt;Component
                {...props}
                animate={{ y: isInView ? <span class="hljs-number">0</span> : -slideDistance }}
                transition={{ duration: <span class="hljs-number">0.2</span>, delay: <span class="hljs-number">0.25</span>, ease: <span class="hljs-string">"easeInOut"</span> }}
            /&gt;
        )
    }
}
</code></pre>
<hr />
<h2 id="heading-time-to-put-it-all-together">Time to put it all together</h2>
<p><strong>Congratulations 🎉</strong>. You are a scroll master. With the techniques and concepts you’ve learned here, you have the building blocks of almost every kind of scroll interaction.</p>
<p>To see these overrides in action and more <a target="_blank" href="https://tools-paint-059317.framer.app">check out this example project</a>.</p>
<p><a target="_blank" href="https://www.framer.com?via=giles_perry"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669125852141/79MLwBwLG.png" alt="Partner Banner Color@2x.png" /></a></p>
<hr />
<h2 id="heading-a-bit-about-me">A bit about me</h2>
<p>My name is Giles Perry. I’m a Principal Product Designer at Skyscanner. Check out my <a target="_blank" href="https://gilesperry.info">website</a> or find me on <a target="_blank" href="https://www.linkedin.com/in/gilesperryuserexperience/">LinkedIn</a>.</p>
<p>Like this article and <strong>want to say thanks</strong>? Why not <a target="_blank" href="https://www.paypal.me/perrysmotors/2">buy me a coffee</a> ☕️?</p>
]]></content:encoded></item></channel></rss>