Scroll and Page Animations

This content was generated with the assistance of AI and should be verified against the official Webflow documentation before being relied on in production.

This sub-section’s Webflow bibliography lists the reference material consulted while preparing these pages.

This page was checked against the Webflow Help Center search results, the Webflow Designer documentation and the GSAP documentation on 2026-10-28. Webflow is a hosted service without version numbers, so a panel label can change between two visits. Interactions with GSAP are available in the Designer of every plan, including the free Starter Site plan in a Starter Workspace, so nothing on this page depends on a paid tier; custom code in a site-wide head or footer and the Code Embed element that carries the scripts below are described in Custom Code, Head, Footer and Embeds. The custom code examples pin GSAP and ScrollTrigger 3.15.0, the version shown by the GSAP documentation and published on the jsDelivr CDN at the time of the check.

Scroll is the most natural trigger a web page has. The visitor already moves down the page; an animation that is tied to that movement can reveal content, fix a section in place while a story unfolds, move a background more slowly than the text above it, or hide a header that is in the way. This page explains how Webflow models scroll and page-load animation, how to rebuild three classic recipes (parallax, a header that hides while scrolling down, a back-to-top button), what to write yourself in a Code Embed when the panel is not enough, and how to keep all of it fast and comfortable for people who prefer less motion.

The running example is the Fernbrook Gardens site, a fictitious botanical garden. Its home page has a hero with a large photograph, a "Plants of the season" section with a row of cards, a sticky header with the garden logo and a long events page that needs a way back to the top.

Two engines, one idea

Webflow has two interaction systems. Interactions with GSAP is the default on new sites and is built on the GSAP animation library with its ScrollTrigger plugin. Classic interactions (also called IX2) is the older system that remains for a few element-specific events; it is described in Classic Interactions. The principles of the GSAP panel, such as the trigger, target and action model, the timeline and the presets, are explained in Interactions with GSAP; this page concentrates on what is specific to scroll and to page load.

Every scroll animation, whether it is made in the panel or written by hand, answers the same four questions.

  • What is being watched? A trigger element, usually the element that is also being animated, or its section.

  • When does the animation begin and end? Two positions, a start and an end, each expressed as a place on the trigger element meeting a place on the viewport.

  • How does the scroll position drive the animation? Either the animation plays once when the start is reached, or its progress follows the scrollbar (scrub).

  • Does the element stay put while the visitor scrolls? A pinned element is fixed in the viewport for the length of the scroll range.

The rest of the page uses these four questions as a checklist.

Scroll triggers

A scroll trigger links an animation to the position of an element in the viewport. The Webflow panel offers two broad families of scroll behaviour, and the GSAP documentation names them with their plugin properties.

Behaviour What the visitor sees ScrollTrigger property

Play when the element enters the viewport

The animation runs by itself, at its own speed, once the start position is reached

toggleActions, with scrub left off

Follow the scroll (continuous)

The animation advances and rewinds exactly as the visitor scrolls

scrub: true, or a number of seconds of smoothing

Fix the element while scrolling

A section stays in the viewport for a stretch of scrolling, with the page below it waiting

pin: true

React at a boundary

A class is toggled or a function is called when a boundary is crossed

toggleClass, onEnter, onLeave, onEnterBack, onLeaveBack, onToggle

Start and end

Start and end are the most important settings because they define the range in which the scroll can affect the animation. Each is a pair of positions written as two words: the first is a place on the trigger element and the second is a place on the viewport (the scroller). The value "top 80%" means the moment when the top of the trigger reaches the point 80% of the way down the viewport. The default start is "top bottom" (the trigger’s top meets the viewport bottom, so the element has just come into view) and the default end is "bottom top" (the trigger’s bottom meets the viewport top, so the element has just left).

Value Reading

"top 80%"

Trigger top meets a line 80% down the viewport: the element is well into view

"center center"

Trigger centre meets viewport centre: the element is centred

"top top"

Trigger top touches the top of the viewport: the usual start of a pinned section

"+=600"

End 600 px of scrolling after the start

"bottom 20%"

Trigger bottom meets a line 20% from the top of the viewport

The GSAP documentation lists the other properties that choose the range: trigger, endTrigger (a different element for the end), scroller (a scrolling container other than the window) and horizontal. When the scroll range is too awkward to describe in pixels, set markers: true while building: ScrollTrigger draws coloured lines at the start and end on the page, which is the fastest way to see where the boundaries actually are. Remove them before publishing.

The Webflow panel exposes the same idea in its Animation boundaries and presets. An element that is already in view at page load never "enters", so a trigger whose start is above the first screen can appear not to fire until the visitor scrolls; setting the start so it is reached on load, or using a page-load trigger for first-screen content (below), avoids the surprise.

Toggle actions

toggleActions is a string of four action words, one for each of the four moments a trigger can cross a boundary, in this order: onEnter, onLeave, onEnterBack, onLeaveBack. The default is "play none none none". Each word is one of play, pause, resume, reset, restart, complete, reverse or none.

Value Behaviour

"play none none none"

Play once when the element enters from below; nothing else happens

"play none none reverse"

Play on entering and reverse when the visitor scrolls back above the start

"restart none none reset"

Run again each time the element is entered from below and reset it when it is scrolled back out

"play pause resume reset"

Pause when leaving at the bottom and resume when coming back

For a reveal that should run only once, once: true is the shortest option: the trigger kills itself after the first time it fires, which also frees its work. Use a reversible value when the same card is revealed again and again as the visitor scrolls up and down the long events page, because replaying a big movement every time can be tiresome.

Scrub

With scrub: true the animation has no time of its own: its progress is the scroll progress between the start and the end. Scrolling down plays it forward, scrolling up plays it backward and stopping scrolling stops it. A number such as scrub: 0.6 adds a catch-up delay of that many seconds, which smooths out jumpy mouse-wheel scrolling at the cost of a little lag.

Scrub is the right tool for parallax, progress indicators and sequences that tell a story along the page. It is the wrong tool for a short reveal that just needs to appear: a reveal tied to the scrollbar looks half-finished whenever the visitor stops in the middle of it.

Pin

pin: true makes ScrollTrigger fix the trigger element in the viewport when the start is reached and release it at the end. By default it inserts a spacer of the same height as the pinned element so the page below keeps its length; pinSpacing: false removes that spacer. Pinning combines well with scrub: a section is held in place while a timeline inside it advances, and the visitor scrolls through a short story before the page continues.

Pinning has three practical requirements. The pinned element must have a defined size, because the spacer copies it. An ancestor with overflow: hidden or a transform can change how fixed positioning behaves, so avoid them above a pinned section (the positioning rules are in Positioning and Overflow). And a pinned section needs a clear exit: if the range is very long on a small phone, the visitor feels stuck, so keep the pin short and consider turning it off on narrow screens, as the performance section shows.

A summary of the panel and the code

Need In the Designer panel In custom GSAP code

Reveal on entering the viewport

A scroll trigger on the element with an entrance preset

A tween with scrollTrigger: { trigger, start, toggleActions }

Tie progress to the scrollbar

The continuous (while scrolling in view) option and its boundaries

scrub: true or a number

Hold a section in place

Pinning of the trigger element in the interaction

pin: true with end: "+=N"

Run different effects on desktop and phone

Interaction breakpoints

gsap.matchMedia()

Respect reduced motion

The reduced-motion setting of the interaction

(prefers-reduced-motion: reduce) condition

The two columns are not rivals. The panel is better for the usual reveals, because a designer who is not a developer can edit it and it is stored with the design; code is better when the logic is conditional, data-driven or reused many times.

Page-load sequences

A page-load interaction runs when the page has loaded, without any scrolling. It is the right place for the first impression: the headline of the Fernbrook home page fading up, followed by the sub-title, then the call-to-action button, then a gentle zoom-out of the hero photograph. Three ideas decide whether the result looks designed or random.

  • Sequence the elements, do not start them all at once. A timeline in which each step begins slightly after the previous one (an offset or a stagger) reads as one gesture; a dozen simultaneous fades read as flicker.

  • Set the initial state before the first paint. If the headline starts visible and the animation then moves it to hidden and back, the visitor sees a flash. The panel sets the initial state of the target (opacity zero, shifted down) when the interaction is created, so keep the initial state in the interaction rather than in a manual class; for code, hide with CSS in the head and let the script reveal, as in the examples below.

  • Keep it short. Page-load motion delays the moment the content is readable. A total of roughly a second is plenty, and the largest visible element (the hero photograph, usually the Largest Contentful Paint element) should not be hidden at the start, or the metric gets worse; see Performance Optimization.

Content that the visitor can see without scrolling needs a page-load trigger, not a scroll trigger, because a scroll trigger waits for a position that the first screen has already passed. Content further down uses a scroll trigger, and a very long page may combine both: a page-load sequence for the hero and scroll reveals for everything else.

Recipes rebuilt with GSAP

The three recipes below are the traditional scroll effects of a marketing site. They are described in terms of the Fernbrook pages and the GSAP model, and each recipe lists what is done in the Designer and what the equivalent code does. They are written as procedures that apply the principles above; follow the panel for a no-code version and the code for a custom version.

Parallax

Parallax makes a background move more slowly than the foreground, which gives the page depth. In GSAP it is a scrubbed tween of a vertical offset while the hero section crosses the viewport.

  1. Give the hero section the class hero with overflow: hidden and a fixed height, so the moving photograph never shows an edge.

  2. Put the photograph in a child element, hero_image, taller than the hero (about 120% of its height) and positioned with its top at zero.

  3. Create a continuous scroll interaction on the hero as the trigger. Its start is when the hero’s top meets the viewport top (or when it enters), its end is when its bottom leaves the top of the viewport.

  4. Animate the photograph from a small negative vertical offset to a small positive one (a few tens of pixels in total), with no easing, so the speed follows the scroll exactly.

Two tips: use a transform (move) rather than the background position, because transforms are cheap for the browser to animate, and keep the total travel small. A large travel on a phone is the commonest reason a parallax hero feels sluggish.

Header that hides while scrolling down

A sticky header that disappears on the way down and returns on the way up recovers screen space without losing navigation. The effect is not a continuous animation: it depends on the direction of scroll, so it is better expressed as two reactions to the direction.

  1. The header has the class site-header with a fixed or sticky position and a high z-index.

  2. Define a hidden state: moved up by its own height (a percentage such as -100% is independent of the actual height).

  3. When the visitor scrolls down past the header height, play the hidden state; when they scroll up, reverse it.

  4. Keep the header visible whenever the page is at the very top, whatever the last direction was, so the first screen never starts without navigation.

In the panel this is built from a scroll trigger on the page (the whole body) with separate actions for scrolling down and up; the direction-dependent reaction is the part that is awkward in the panel and simple in code, which is shown in the Code Embed section. Also keep keyboard focus in mind: a header that is hidden while a link inside it has keyboard focus should show again, otherwise a keyboard user tabs into an invisible link.

Back-to-top button

A back-to-top button is shown once the visitor has scrolled a long way and returns them to the top. Fernbrook uses it on the events page, which lists a hundred dates.

  1. Add a link element at the bottom of the page with the class to-top, a visible text label such as "Back to top" and a link target of #top on a body or page-wrapper element given the id top. A plain in-page link works without JavaScript, and it is the fallback for visitors with scripts disabled.

  2. Set its initial state to hidden (opacity zero and not clickable).

  3. Create a scroll trigger whose start is a scroll distance of about one screen from the top. Entering plays the entrance (opacity to one); leaving upward reverses it.

  4. For smooth scrolling to the top, use the browser’s scroll-behavior: smooth in CSS rather than a script, and honour the reduced-motion setting by switching it off, as shown below.

The button must remain a real link or button element with a text label, because it is a navigation control: an icon alone needs an accessible name; see Accessibility.

Custom GSAP and ScrollTrigger code in a Code Embed

The panel covers the most common effects. Custom code is useful when an effect depends on direction, on data from the CMS or on conditions that the panel does not model: when the same animation must be built for every card in a Collection List with a different trigger each, for a counter that increments, for a header that reacts to direction, or for a reduced-motion rule that applies to many effects at once. The Fernbrook examples below use the plugin properties documented by GSAP and the Code Embed element of Webflow.

Webflow runs its own GSAP for the interactions made in the panel, but that copy is not documented as an interface for custom code, so load your own pinned copy rather than depending on a global that may or may not be present. Loading and ordering of scripts is covered in Third-party Libraries and Scripts and the general use of custom attributes with scripts in Custom Attributes and JavaScript.

Loading GSAP and ScrollTrigger

Put the two scripts in the footer code of the page (or the site) so they load after the page content, in this order, and register the plugin once:

<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script>
  gsap.registerPlugin(ScrollTrigger);
</script>

The version is part of the URL on purpose. A URL without a version follows the newest release, which can change an animation on a live site without anyone touching Webflow. Pin the version, and raise it deliberately after previewing the result. The documentation states that ScrollTrigger requires GSAP 3.3.0 or later and the registration call is the only setup the plugin needs.

A one-time reveal of the plant cards

The first example reveals each card of a Collection List once, as it comes into view. The cards start hidden by a CSS rule in the page head, so there is no flash before the script runs, and the script moves them to their final state.

<style>
  /* Hidden only when JavaScript is available; see the .js class set below. */
  .js .plant-card { opacity: 0; transform: translateY(24px); }
</style>
<script>
  document.documentElement.classList.add("js");
</script>

Placing the js class on the root element makes the hidden state apply only when scripts run; a visitor whose script fails still sees the cards. The animation itself goes in a Code Embed at the end of the page or in the footer code:

// Reveal each .plant-card once, when it is 85% down the viewport.
gsap.utils.toArray(".plant-card").forEach((card) => {
  gsap.to(card, {
    opacity: 1,
    y: 0,
    duration: 0.7,
    ease: "power2.out",
    scrollTrigger: {
      trigger: card,
      start: "top 85%",
      once: true
    }
  });
});

Notes on this code: gsap.utils.toArray turns the matched elements into an array, one trigger per card keeps each reveal independent of the others, and once: true removes the trigger after it fires. Because the CSS hides the cards with a translate and the tween animates y to 0, the final state is the natural layout the Style panel designed.

A scrubbed parallax hero

This tween moves the hero photograph between two small offsets while the hero crosses the viewport.

// The photograph travels 60px over the whole time the hero is on screen.
gsap.fromTo(".hero_image",
  { y: -30 },
  {
    y: 30,
    ease: "none",
    scrollTrigger: {
      trigger: ".hero",
      start: "top bottom",
      end: "bottom top",
      scrub: true
    }
  }
);

ease: "none" matters: any other easing bends the relationship between scroll and movement, and the effect no longer feels attached to the scroll. A numeric scrub such as 0.5 adds smoothing if the visitor uses a stepped mouse wheel.

A pinned story section

The next example holds the "Seasons in the garden" section for the length of two screens while four text steps fade in and out, a pattern that tells a story without a slider.

const steps = gsap.utils.toArray(".season-step");

const story = gsap.timeline({
  scrollTrigger: {
    trigger: ".seasons",
    start: "top top",
    end: "+=2000",
    scrub: 0.5,
    pin: true
  }
});

steps.forEach((step, i) => {
  story.fromTo(step, { opacity: 0 }, { opacity: 1, duration: 1 }, i * 1.5);
  if (i < steps.length - 1) {
    story.to(step, { opacity: 0, duration: 1 }, i * 1.5 + 1.25);
  }
});

A timeline placed under a single ScrollTrigger spreads its total duration over the scroll range, so the numbers inside the timeline set proportions, not seconds. end: "+=2000" is the length of the pinned scroll in pixels.

A direction-aware header

The last example hides the header on the way down and shows it on the way up. It uses the callbacks of a page-wide ScrollTrigger, which report the direction, instead of an animation tied to a position.

const header = document.querySelector(".site-header");
const hide = gsap.to(header, { yPercent: -100, duration: 0.3, ease: "power2.out", paused: true });

ScrollTrigger.create({
  start: 0,
  end: "max",
  onUpdate: (self) => {
    // self.direction is 1 when scrolling down and -1 when scrolling up.
    if (self.direction === 1 && self.scroll() > header.offsetHeight) {
      hide.play();
    } else {
      hide.reverse();
    }
  }
});

// Keep the header visible while keyboard focus is inside it.
header.addEventListener("focusin", () => hide.reverse());

The header must have a position that keeps it in the viewport (fixed or sticky) for its movement to be visible. If it also has a background colour on scroll, the same callback can toggle a class with the style change, which keeps visual details in the Style panel.

A back-to-top button

A trigger with toggleClass is enough for this control; the Style panel defines how the class looks.

ScrollTrigger.create({
  trigger: document.body,
  start: "top -100%",     // after scrolling one viewport height
  end: "max",
  toggleClass: { targets: ".to-top", className: "is-visible" }
});

The is-visible class is a combo class on to-top that sets opacity to one and enables clicks; the Style panel can make it with the usual combo class mechanism described in Classes, Combo Classes and Selectors. Using a class toggle for a simple on-off state is more robust than animating from code because the visual design stays in the Designer.

Where the code goes and how to test it

A Code Embed element holds HTML, CSS and JavaScript inside the page, and the footer code of the page or site holds scripts that apply more broadly. The embed is not executed in the Designer canvas, so previewing and publishing to the webflow.io staging domain is the test; see Publishing and Scheduling. The usual troubleshooting list:

  • Nothing happens. The plugin is not registered, or the script runs before the elements exist. Place the code in the footer or in an embed after the elements.

  • Triggers fire at the wrong place. Images and fonts that finish loading after the script change the page height. Call ScrollTrigger.refresh() after late layout changes, such as the end of a font load.

  • Selectors match nothing. Webflow class names are the names in the Style panel; combo classes are separate class names on the same element.

  • A conflict with a panel interaction. Do not animate the same property of the same element from both the panel and code; the later one overrides the earlier one.

Performance

A scroll animation runs as often as the visitor scrolls, so the cost of each frame matters more than for a one-time reveal.

  • Animate transforms and opacity. Movement, scaling, rotation and opacity do not force the browser to recalculate layout. Animating top, left, width, height or margins does, on every frame. The GSAP properties x, y, scale, rotation and opacity are transforms and opacity. The background of CSS animation performance is explained in CSS Animations, Keyframes and Performance.

  • Limit the number of simultaneous scrubbed animations. A dozen parallax layers on one phone is usually too many.

  • Prefer fewer, bigger effects. A single well-chosen pinned section is better than five small ones.

  • Mind the images. An animated image still has to be decoded; large photographs make parallax stutter. Size them as described in Images and Assets.

  • Do not use will-change everywhere. Apply it only to elements that move during a scroll range, and remove it when the effect ends.

  • Kill triggers you no longer need. once: true and kill() free the work of finished effects.

  • Test on a real phone. A desktop browser on a fast machine hides problems. Throttle the CPU in the browser developer tools or use a mid-range phone.

  • Watch the layout metrics. Content that moves in after the load should not push other content: reserve its space, otherwise Cumulative Layout Shift worsens.

An effect that is only decoration is better switched off on narrow screens. gsap.matchMedia() creates animations inside a function that runs only when a media query matches and reverts them automatically when it stops matching:

const mm = gsap.matchMedia();

mm.add("(min-width: 992px)", () => {
  // Desktop and wider: the pinned story and the parallax.
  gsap.fromTo(".hero_image", { y: -30 }, {
    y: 30, ease: "none",
    scrollTrigger: { trigger: ".hero", scrub: true }
  });
});

mm.add("(max-width: 991px)", () => {
  // Phones and tablets: a single, cheap fade.
  gsap.from(".hero_image", { opacity: 0, duration: 0.6 });
});

The breakpoints of the Designer are described in Responsive Design and Breakpoints; use the same widths in the media queries so the code and the Style panel agree about what "tablet" means.

Motion sensitivity and prefers-reduced-motion

Large movement across the screen, parallax, spinning and anything that moves with the scroll can cause discomfort, dizziness or nausea for people with vestibular disorders, and others find constant motion distracting. Operating systems provide a setting for this, and browsers expose it to CSS and JavaScript as the prefers-reduced-motion media feature. A site should treat the setting as a request to remove non-essential motion, not as a request to remove all change: a fade or a colour change is usually fine, while a large translation or a scrubbed parallax is not.

Three layers make a site respectful.

  1. The Webflow interaction settings. The interaction system provides a reduced-motion setting for the animations created in the panel; check the interaction’s options for how it responds, and confirm it in a browser with the setting turned on, since the behaviour of the default is a decision of the product that can change.

  2. CSS. Turn off smooth scrolling and CSS animations in a media query in the site’s custom code:

    @media (prefers-reduced-motion: reduce) {
      html { scroll-behavior: auto; }
      .plant-card { transition: none; }
    }

    Source: GSAP documentation: gsap.matchMedia() (the same media feature, used from JavaScript). The CSS side is explained in CSS Animations, Keyframes and Performance.

  3. Your own GSAP code. Register the full-motion effects only when the visitor has no preference, and give the reduced case a gentler alternative or nothing:

const mm = gsap.matchMedia();

mm.add(
  {
    full: "(prefers-reduced-motion: no-preference)",
    reduced: "(prefers-reduced-motion: reduce)"
  },
  (context) => {
    const { full } = context.conditions;

    if (full) {
      gsap.fromTo(".hero_image", { y: -30 }, {
        y: 30, ease: "none",
        scrollTrigger: { trigger: ".hero", scrub: true }
      });
    }

    // Reduced motion: the content is simply visible, no movement at all.
    gsap.set(".plant-card", { opacity: 1, y: 0 });
  }
);

The handler reruns when the visitor changes the operating-system setting while the page is open, and the animations created before are reverted automatically, so a person who switches the setting does not have to reload. The gsap.set line shows the safe default for the reduced case: all content visible in its final position.

A few further rules:

  • Never hide content that only an animation can reveal; a visitor who cannot see the animation (script blocked, reduced motion) must still be able to read the page.

  • Avoid flashing content: no element should flash more than three times in a second.

  • Do not hijack the scroll. A pinned story that makes the page feel stuck, or a scroll that moves at a different speed from the visitor’s gesture, harms usability as well as accessibility.

  • Offer a pause for motion that lasts more than five seconds and starts by itself.

  • Test with the operating-system setting turned on (reduce motion on macOS and iOS, remove animations on Windows and Android) and with the keyboard only.

Pitfalls

  • Scrubbing a short reveal. Tying a small fade to the scrollbar makes it look half-finished whenever scrolling stops. Use toggle actions for reveals and scrub for continuous stories.

  • Start above the first screen. An element already in view on load never crosses its start; use a page-load trigger or set the start so it is reached on load.

  • Late layout changes. Images, fonts and CMS content that load after the script move the positions; refresh the triggers afterwards.

  • Pinning inside a transformed or overflow-hidden parent. Fixed positioning is affected by these; keep pinned sections as direct children of the page wrapper.

  • Animating layout properties. Width, height and margins are expensive at 60 frames per second.

  • Unpinned library versions. A CDN URL without a version silently changes behaviour.

  • Doubling up. Panel and code both animating the same element lead to hard-to-trace results.

  • Ignoring reduced motion. A site that ignores the setting excludes some visitors and can fail accessibility reviews.

  • Classic scroll on iOS. Classic scroll interactions have long been limited on iOS because the browser does not let scripts run during a scroll; the GSAP-based system and ScrollTrigger use a different mechanism, so test the actual device.

Corrections to older material

Books and tutorials from the Classic era describe scroll effects as the "Scroll into view" and "While scrolling in view" triggers of the interactions panel, with separate recipes for parallax, a hiding navigation bar and a back-to-top button, each built from fixed offsets and a timeline of timed steps.

  • "Scroll animations are built only from Classic triggers." New sites use Interactions with GSAP by default, which includes ScrollTrigger and a timeline model; Classic remains for a few element-specific events.

  • "Scroll interactions need a lot of timeline steps." A single continuous interaction or a tween with scrub: true expresses the same parallax.

  • "The header recipe is a toggle of two states." Direction-aware behaviour is simpler in ScrollTrigger callbacks.

  • "Motion preferences are not a design matter." Reduced-motion handling is now part of the baseline expectation of an accessible site.

Where to go next