Overview
What is included
- GSAP Marquee — simple GSAP loop for horizontal scrolling content, both directions.
- GSAP Scroll Interactions — built inside Webflow Interactions, triggered by data attributes.
- Font Smoothing — minimal global font-smoothing declarations.
Optional add-ons
- Lenis Smooth Scroll — paste-in snippet, integrates with the GSAP ticker.
- Infinite Slider — paste-in snippet that adds seamless looping to the native Webflow slider.
Required libraries
Where the code lives
GSAP Marquee
data-marquee attribute.What it does
Build the marquee content as a single horizontal track that contains your items duplicated once — so the loop can travel the full width of the original set and then snap back invisibly. Add one of these attributes to the track:
data-marquee="left"— moves right to left.data-marquee="right"— moves left to right.
Tuning
- Speed — change the
durationvalue. Higher = slower. Lower = faster. - Direction — pick the attribute that matches the direction you want.
- Why -50% — because the track contains the item set duplicated, moving by half its width loops back to the visually identical starting point.
The script
<script>
gsap.to('[data-marquee="left"]', {
x: "-50%",
duration: 20,
repeat: -1,
ease: "none"
});
gsap.fromTo('[data-marquee="right"]',
{ x: "-50%" },
{ x: "0%", duration: 20, repeat: -1, ease: "none" }
);
</script>How to remove a marquee
data-marquee attribute from the track, or remove the script block to disable all marquees on the page.GSAP Interactions
For quick reference, the six attributes and where each one goes:
data-gsap-scroll="fade"— single element fade-in.data-gsap-scroll="fade-late"— delayed fade-in.data-gsap-scroll="stagger"— parent element, animates direct children.data-gsap-scroll="text"— text wrapper, runs heading split + tag fade + body reveal.data-parallax="medium"— wrapper with.media-fillchild, subtle parallax.data-parallax="strong"— same, stronger parallax.
Why data attributes
- Non-destructive — adding or removing an attribute never affects an element's classes or styling.
- Reusable — the same animation can be applied to any element on any page without touching the interaction itself.
- Easy to audit — you can scan the Webflow Designer's attribute panel to see which elements animate and how.
Where the interactions are defined
[DATA]. They use Webflow's "Element Trigger → On Scroll" with the trigger set to a custom attribute selector. The animation logic itself is built with GSAP timelines inside the Action.Fade
A simple fade-in. The element appears with an opacity transition as it enters the viewport. It is the workhorse animation of the template and the safest default when you want something to feel intentional without drawing attention to the motion itself.
- Attribute:
data-gsap-scroll="fade" - Best for: images, individual cards, body text, single buttons, section content blocks.
- Visual: the element fades from invisible to fully visible as it enters the viewport.
- To remove: delete the
data-gsap-scrollattribute from the element.
Fade Late
Identical to Fade, but slightly delayed. Use it when you want one element to come in just after another for a more natural reading flow — for example a paragraph after its heading, or a CTA after its supporting copy.
- Attribute:
data-gsap-scroll="fade-late" - Best for: the second or third element in a small group where Fade is already used.
- Visual: same fade-in as Fade, with an added delay so it follows the previous element.
- To remove: delete the attribute, or change it to
fadeto remove only the delay.
Stagger
Animates multiple child elements in sequence. The trigger element itself does not animate — its direct children do, one after the other. This is the right choice for any list-like layout where items should reveal as a group.
- Attribute:
data-gsap-scroll="stagger"(placed on the parent / wrapper element). - Best for: grids of cards, feature lists, gallery rows, navigation lists, tag groups.
- Visual: children fade and slide in one after another in source order.
- To remove: delete the attribute from the parent. Children return to their static state.
- Note: the script targets direct children, so wrap your items in a single parent and avoid nesting extra wrapper divs around individual items.
Text
A composite text animation built for typographic content. It runs three coordinated actions: a heading split, a tag fade, and a body text reveal. Use it on text-heavy sections where you want a refined, progressive reading experience.
- Attribute:
data-gsap-scroll="text"(on the section or text wrapper). - Best for: hero sections, section intros, editorial blocks, anywhere a heading, eyebrow tag and paragraph appear together.
- Visual: the heading reveals in a split, the eyebrow tag fades in, and the supporting body text animates in sequence.
- To remove: delete the attribute from the wrapper.
- Note: works best when applied to a wrapper that contains a heading, an optional tag and supporting text. If the wrapper holds only one of these, the unused actions simply have nothing to animate.
Parallax Media
Adds a subtle vertical parallax movement to media as the viewport scrolls past it. The interaction looks for an inner .media-fill element inside the trigger and moves it relative to the scroll position. The outer trigger element does not move — only the media inside.
- Attribute:
data-parallax="medium" - Required structure: the trigger element must contain a child with the class
.media-fill. - Best for: hero images, section background imagery, large editorial photos, any framed media.
- Visual: the inner media drifts vertically against the scroll, giving the section gentle depth.
- To remove: delete the attribute from the trigger element.
Parallax Media Strong
Same mechanism as Parallax Media, but with a stronger, more noticeable movement. Use it sparingly — typically once per page, on a single feature image. Overusing strong parallax across multiple sections quickly feels heavy.
- Attribute:
data-parallax="strong" - Required structure: same as Parallax Media — needs a child
.media-fill. - Best for: a single hero or signature image per page.
- Visual: the inner media drifts more visibly than the medium variant.
- To remove: delete the attribute, or change it to
mediumto soften the effect.
Light and Dark
body to invert the entire site. Instead, dark sections are achieved with Webflow Variable Modes — specifically, by setting a section's variable color mode to Inverted Mode.How it works
- The template defines all colors as variables — surface, text, border, accents — rather than hard-coded hex values.
- Light sections use the default variable mode.
- Dark sections are individual sections with their variable mode set to Inverted. Every variable inside that section resolves to its inverted equivalent.
- Child elements automatically inherit the inverted values, so no extra dark-mode classes are needed.
Why this approach
Customizing dark colors
Adding a new dark section
- Add or duplicate a section in the Designer.
- Select the section and switch its Variable Mode to Inverted.
- Do not assign any extra dark-mode utility classes to the children — they will resolve correctly through inheritance.
What to avoid
SVG Icons and Logos
Why Code Embeds, not images
- currentColor — inline SVGs can use
fill="currentColor"andstroke="currentColor", which inherits the CSScolorof the parent. The icon adopts whichever variable is active in that section. - Image uploads can't do this — media browser SVGs render as
<img>tags, which cannot inherit colors. - Sharpness and scaling — same advantages as any SVG: clean at any size, any pixel density.
How to swap an icon
- Select the icon, click the gear icon on the Code Embed.
- Delete the existing code, drag your new SVG file into the code box. If drag-drop fails, open the SVG in a text editor and paste the code in.
How to make any new SVG fit the template
Most SVG files ship with hard-coded colors and explicit dimensions. A few edits make them fit the system:
- Recolor — replace every
fill="#..."andstroke="#..."withfill="currentColor"andstroke="currentColor". The icon now follows the active color variable. - Stroke width — for stroked icons, replace any hard-coded
stroke-width="..."withstroke-width="var(--_⚡️-icons---icon-strk)". The stroke is then driven by the global icon-stroke variable, so a single change in the variable updates every icon at once. - Resize — remove the
widthandheightattributes from the opening<svg>tag. Keep theviewBox. The icon now respects the size set by its parent class.
Quick checklist
- Code Embed, not Image element.
- All colors →
currentColor. widthandheightremoved from the SVG tag,viewBoxkept.- Logos only:
style="height:100%;"added, unique class with ratio set.
How to Remove or Disable Effects
Remove smooth scrolling
Remove the marquee
data-marquee attribute from individual elements you want to stop animating.Remove a single scroll animation
data-gsap-scroll or data-parallax attribute from the element.Remove all scroll animations
[DATA]. The data attributes can stay on the elements — without an interaction listening to them, they have no effect.Remove the type smoothener
108™ Type Smoothener style block from the global custom code. Typography returns to browser defaults.Best Practices
Don't duplicate global scripts
Use attributes over classes for animation
Keep the original markup intact
.is-clone class from cloned slides at runtime.Use variables, not hex codes
Test scroll animations in Preview
Infinite Slider (Optional)
Setup
- In the Webflow slider settings, uncheck "Infinite repeat slides". The script handles looping.
- The required custom attributes are already added to the slider in this template:
data-slider-loop="true"— enables the loop logic.data-slider-clones="3"— number of slide sets cloned on each side. Default 3.data-slider-start="0"— index of the original slide visible on load. Zero-based.
- Paste the script below before the closing
</body>tag in Project Settings → Custom Code, or in the page's custom code area.
The script
<script>
/**
* 108™ Infinite Slider
*
* What this is:
* - An infinite loop system for native Webflow sliders.
*
* What it does:
* - Clones the original slide set before and after the real slides.
* - Redraws the Webflow slider after DOM changes.
* - Reinitializes IX2 if cloned slides contain interactions.
* - Sets the chosen starting original slide.
*
* Instructions:
* - Add these attributes to the slider element:
* - data-slider-loop="true"
* - data-slider-clones="3"
* - data-slider-start="0"
*
* Example:
* - <div class="w-slider"
* data-slider-loop="true"
* data-slider-clones="3"
* data-slider-start="0">
*
* Notes:
* - data-slider-clones = number of full slide sets added on each side.
* - data-slider-start = starting ORIGINAL slide index, based on 0.
* - This script runs on window load for more reliable Webflow timing.
*/
$(window).on('load', function () {
$('[data-slider-loop="true"]').each(function () {
var $slider = $(this);
/* Prevent double initialization in Webflow re-runs. */
if ($slider.attr('data-clones-initialized') === 'true') return;
$slider.attr('data-clones-initialized', 'true');
/* Read clone count from attribute and fall back safely. */
var cloneCount = parseInt($slider.attr('data-slider-clones'), 10);
if (isNaN(cloneCount) || cloneCount < 1) cloneCount = 3;
/* Read starting original slide index and fall back safely. */
var startIndex = parseInt($slider.attr('data-slider-start'), 10);
if (isNaN(startIndex) || startIndex < 0) startIndex = 0;
/* Find the native Webflow slider mask. */
var $mask = $slider.find('.w-slider-mask').first();
if (!$mask.length) return;
/* Use only original slides, not previously cloned ones. */
var $slides = $mask.children('.w-slide').not('.is-clone');
var originalCount = $slides.length;
if (!originalCount) return;
/* Clone original slides before the real set. */
for (var i = 0; i < cloneCount; i++) {
$slides.clone().addClass('is-clone').prependTo($mask);
}
/* Clone original slides after the real set. */
for (var j = 0; j < cloneCount; j++) {
$slides.clone().addClass('is-clone').appendTo($mask);
}
/* Redraw native Webflow components after cloning. */
function redrawSlider() {
try {
if (window.Webflow && Webflow.require) {
Webflow.require('slider').redraw();
try {
Webflow.require('tabs').redraw();
} catch (e) {}
}
} catch (e) {}
}
/* Reinitialize IX2 in case cloned slides contain interactions. */
function reinitIx() {
try {
if (window.Webflow && Webflow.require) {
Webflow.require('ix2').init();
}
} catch (e) {}
}
/* Move to the selected ORIGINAL slide using native nav dots. */
function setStartSlide() {
var $navDots = $slider.find('.w-slider-nav').children();
var safeStartIndex = Math.min(startIndex, Math.max(originalCount - 1, 0));
if ($navDots.length && $navDots.eq(safeStartIndex).length) {
$navDots.eq(safeStartIndex).trigger('click');
}
}
/* Run setup in short phases for more reliable Webflow timing. */
redrawSlider();
setTimeout(function () {
redrawSlider();
setTimeout(function () {
reinitIx();
setTimeout(function () {
redrawSlider();
setStartSlide();
}, 120);
}, 120);
}, 120);
});
});
</script>Tuning
- Clone count — increase if the slider is very wide or slides are narrow, decrease if it feels heavy.
- Start index — pick which original slide is active on load. Out-of-range values are clamped.
To remove
Smooth Scroll (Optional)
What it does
- Make sure GSAP and ScrollTrigger are loaded before this snippet.
- Only one global smooth scroll instance per page — do not duplicate.
The script
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lenis@1.2.3/dist/lenis.css" />
<script src="https://cdn.jsdelivr.net/npm/lenis@1.2.3/dist/lenis.min.js"></script>
<script>
/**
* 108™ Smooth Scroll
*
* What this is:
* - Lenis smooth scrolling connected to GSAP.
*
* What it does:
* - Creates smoother page scrolling.
* - Keeps ScrollTrigger in sync with Lenis.
* - Uses GSAP's ticker to drive Lenis on every frame.
*
* Instructions:
* - Load Lenis before this script.
* - Load GSAP and ScrollTrigger before this script.
* - Add this globally in the project head or before the closing body tag.
* - Use only one global smooth scroll setup per page.
*/
const lenis = new Lenis();
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
</script>