This challenge’s been turning heads—and for good cause. Warhol Arts isn’t simply visually putting; it’s filled with intelligent interactions, refined movement, and an entire lot of Webflow and GSAP wizardry beneath the hood. So we requested Serhii Polyvanyi, founder and inventive director at BL/S®, to take us behind the scenes. On this deep dive, he breaks down how the challenge went from a enjoyable little Dribbble shot to a full-blown digital expertise—protecting every thing from customized animations and efficiency methods to the artistic course of that held all of it collectively.
From a Tiny Dribbble Shot to a Full-Blown Digital Spectacle
This started as a easy Dribbble shot—only a enjoyable, inner experiment. However then our founder took Niccolò Miranda’s course, which talked about how seamlessly GSAP could possibly be built-in into Webflow—like a Swiss watch. So we put it to the take a look at. Growth—Warhol Arts changed into a full-blown digital wonderland. What began as a aspect challenge advanced into one thing a lot greater: an interactive tribute to the king of pop artwork, Andy Warhol. Daring, sudden, and filled with dynamic GSAP magic.

The Large Thought Behind Warhol Arts
We’re obsessive about Warhol’s fearless creativity. The best way he shook up the artwork world, turned on a regular basis objects into icons, and made us query what artwork even is. So we thought—how can we channel that power into a web site? The reply: fearless design, explosive colours, and animations that don’t simply look cool however inform a narrative.


Animation Highlights
We knew early on that movement would play an enormous function in shaping the character of Warhol Arts. Animation wasn’t simply an afterthought—it was a part of the idea from day one. We wished the positioning to really feel alive, reactive, and sudden, like a digital extension of Warhol’s power. On this part, we’ll stroll by means of among the key interactions that made it occur—from GSAP-driven hero moments and scroll-triggered results to cursor-based typography and playful micro-interactions. Most of those have been constructed utilizing a mix of GSAP, ScrollTrigger, and Webflow Interactions, layered fastidiously to remain performant whereas nonetheless feeling daring and expressive.
Hero-section after preloader Letter
The looks of the letters WARHOL after the preloader is achieved utilizing GSAP, their SplitText plugin and a customized reusable GSAP animation made utilizing registerEffect. It makes use of scale transformations and shade modifications.
window.Webflow ||= [];
window.Webflow.push(() => {
const COLORS_ARRAY = [
"#FB4E2B",
"#FB4E2B",
"#FB4E2B",
"#FB4E2B",
"#FB4E2B",
"#FB4E2B",
"#FB4E2B",
"#FFE5D5",
];
const STEP_DURATION = 0.1;
const textual content = doc.querySelector('[wb-element="rainbow-text"]');
const delay = parseFloat(textual content.getAttribute("data-delay")) || 3.4;
gsap.set(textual content, { autoAlpha: 1 });
const splitText = new SplitText(textual content, { sorts: "chars" });
gsap.set(splitText.chars, { shade: COLORS_ARRAY[0] });
// create our personal customized animation known as changeColor
gsap.registerEffect({
identify: "changeColor",
impact: (targets, config) => {
return gsap.set(targets, { delay: config.period, shade: config.shade });
},
defaults: { period: STEP_DURATION },
extendTimeline: true, // permits the impact immediately on any GSAP timeline
});
gsap.from(splitText.chars, {
scale: 0,
stagger: STEP_DURATION,
delay: delay,
ease: "again.out",
shade: (index, goal) => {
const tlColors = gsap.timeline();
COLORS_ARRAY.forEach((shade) => {
tlColors.changeColor(goal, { period: STEP_DURATION, shade: shade });
});
},
});
});
Click on on the Tube
Clicking on the tube triggers a GSAP animation utilizing the ScrollToPlugin, easily scrolling to the following part’s anchor.

Right here you possibly can see it in motion:
The code seems to be as follows:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/ScrollToPlugin.min.js"></script>
<script>
gsap.registerPlugin(ScrollToPlugin);
doc.querySelector('.button-tube-wrapper').addEventListener('click on', perform(e) {
e.preventDefault();
setTimeout(perform() {
gsap.to(window, {
period: 0.5,
scrollTo: "#4-elvis",
ease: "power2.inOut"
});
}, 1000);
});
</script>
Elvis textual content strikes primarily based on the cursor on the left
Because the cursor strikes, we seize its X-axis coordinates and dynamically modify the font dimension utilizing a GSAP animation, making certain easy and optimized transitions. Moreover, we applied logic to disable the mouse occasion listener exterior this part, protecting the impact contained and environment friendly.
That is it the way it seems to be:


const part = doc.getElementById("4-elvis");
const texts = doc.querySelectorAll(".elvis-text");
const minFontSize = 1.375;
const baseFontSize = 2.125;
const maxFontSize = 2.875;
part.addEventListener("mousemove", (e) => {
const rect = part.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
texts.forEach((textual content) => {
const textRect = textual content.getBoundingClientRect();
const textCenterX = textRect.left + textRect.width / 2;
const distanceFromCursor = Math.abs(mouseX - textCenterX);
const maxDistance = rect.width / 2;
const normalizedDistance = Math.min(distanceFromCursor / maxDistance, 1);
const fontSize =
maxFontSize - (maxFontSize - minFontSize) * normalizedDistance;
gsap.to(textual content, {
fontSize: ${fontSize}em,
period: 0.3,
ease: "power3.out",
overwrite: "auto",
});
});
});
part.addEventListener("mouseleave", () => {
texts.forEach((textual content) => {
gsap.to(textual content, {
fontSize: ${baseFontSize}em,
period: 0.4,
ease: "power3.out",
});
});
});
Textual content animation on scroll within the Monroe part
Webflow Interactions > Whereas Scrolling in View was used right here.




Footer path impact
The waves have been applied utilizing Webflow animations: Webflow Interactions > Whereas Scrolling in View.




Footer path impact
The path impact can also be applied utilizing GSAP, which mixes opacity and scale animations with brightness and distinction properties when the cursor strikes. GSAP optimizes this animation so easily that there aren’t any delays or lags.


showNextImage() {
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.photos[this.imgPosition];
gsap.killTweensOf(img.DOM.el);
img.timeline = gsap.timeline({
onStart: this.onImageActivated.bind(this),
onComplete: this.onImageDeactivated.bind(this)
})
.fromTo(img.DOM.el, {
opacity: 1,
scale: 0,
zIndex: this.zIndexVal,
x: cacheMousePos.x - img.rect.width / 2,
y: cacheMousePos.y - img.rect.top / 2
}, {
period: 0.4,
ease: 'power1',
scale: 1,
x: mousePos.x - img.rect.width / 2,
y: mousePos.y - img.rect.top / 2
}, 0)
.fromTo(img.DOM.inside, {
scale: 2,
filter: 'brightness(300%) distinction(300%)'
}, {
period: 0.4,
ease: 'power1',
scale: 1,
filter: 'brightness(100%) distinction(100%)'
}, 0)
.to(img.DOM.el, {
period: 0.4,
ease: 'power3',
opacity: 0
}, 0.4);
}
404 Web page – Mouse transfer over aspect
Motion of numbers relying on the cursor, applied utilizing Webflow animations.



Textual content stuffed by a masks whereas scrolling
Textual content masks filling utilizing the ScrollTrigger plugin offered by GSAP, which permits us to work together with the positioning by means of scrolling (a small interactive characteristic).


perform runSplit() {
const typeSplit = new SplitText(".split-word", {
sorts: "strains, phrases"
});
doc.querySelectorAll(".phrase").forEach((phrase) => {
const masks = doc.createElement("div");
masks.classList.add("line-mask");
phrase.appendChild(masks);
});
createAnimation();
}
runSplit();
gsap.registerPlugin(ScrollTrigger);
perform createAnimation() {
const allMasks = Array.from(doc.querySelectorAll(".line-mask"));
const tl = gsap.timeline({
scrollTrigger: {
set off: ".split-word",
begin: "high 85%",
finish: "backside middle",
scrub: 1
}
});
tl.to(allMasks, {
width: "0%",
period: 1,
stagger: 0.5
});
}
Basic textual content look throughout the positioning utilizing GSAP
All through our web site, textual content animations are positioned and applied in a approach that they set off when scrolling reaches a selected aspect (utilizing the ScrollTrigger plugin). This method makes it extra optimized, because the animation solely triggers when wanted, quite than operating repeatedly within the background.



window.addEventListener("DOMContentLoaded", () => {
new SplitText("[text-split]", {
sorts: "phrases, chars",
tagName: "span"
});
perform createScrollTrigger(triggerElement, timeline) {
ScrollTrigger.create({
set off: triggerElement,
begin: "high backside",
onLeaveBack: () => {
timeline.progress(0).pause();
}
});
ScrollTrigger.create({
set off: triggerElement,
begin: "high 85%",
onEnter: () => timeline.play()
});
}
$("[text-rotate-fade-in]").every(perform () {
const delay = parseFloat($(this).attr("data-delay")) || 0;
const tl = gsap.timeline({ paused: true });
tl.from($(this).discover(".char"), {
rotation: -45,
opacity: 0,
transformOrigin: "0% 50%",
period: 0.6,
ease: "again.out(2)",
stagger: 0.03,
delay: delay
});
createScrollTrigger($(this), tl);
});
gsap.set("[text-split]", { opacity: 1 });
});
Optimisation: Delays loading of sections after the preloader and hero
This script waits for the complete DOM to load, then screens the disappearance of the aspect with the category .preloader
utilizing MutationObserver
. As soon as the preloader disappears (its show is ready to none), it disables the observer and prompts the lazy sections (.lazy-section
) by including the energetic
class — triggering the deferred content material look after the loading is full.
doc.addEventListener("DOMContentLoaded", () => {
const preloader = doc.querySelector(".preloader");
const lazySections = doc.querySelectorAll(".lazy-section");
const activateLazySections = () => {
lazySections.forEach((part) => {
part.classList.add("energetic");
});
};
const handlePreloaderEnd = () => {
preloader.model.show = "none";
activateLazySections();
};
const preloaderObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.goal.model.show === "none") {
handlePreloaderEnd();
preloaderObserver.disconnect();
}
});
});
preloaderObserver.observe(preloader, { attributes: true, attributeFilter: ["style"] });
});

Animation of tab switching within the modal window in the course of the ticket buy
Because of customized and built-in GSAP easing features, we are able to create animations like slide/billet transitions. On this case, we selected the next easing: const easing = "power1.out"
.


doc.addEventListener("DOMContentLoaded", perform () {
const slides = doc.querySelectorAll(".step");
const nextButton = doc.querySelector(".subsequent");
const prevButton = doc.querySelector(".earlier");
let currentSlideIndex = 0;
const animationDuration = 0.75;
const easing = "power1.out";
slides.forEach((slide, index) => {
gsap.set(slide, {
y: "0%",
zIndex: index === 0 ? 1 : 0,
place: "absolute",
high: 0,
left: 0,
width: "100%",
top: "100%",
visibility: index === 0 ? "seen" : "hidden",
});
});
perform goToSlide(newIndex) {
if (newIndex < 0 newIndex >= slides.size newIndex === currentSlideIndex) return;
const currentSlide = slides[currentSlideIndex];
const nextSlide = slides[newIndex];
gsap.fromTo(
nextSlide,
{ y: "-100%", visibility: "seen", zIndex: 2 },
{ y: "0%", period: animationDuration, ease: easing }
);
gsap.to(currentSlide, {
zIndex: 0,
onComplete: () => {
gsap.set(currentSlide, { visibility: "hidden" });
},
});
currentSlideIndex = newIndex;
}
nextButton.addEventListener("click on", () => goToSlide(currentSlideIndex + 1));
prevButton.addEventListener("click on", () => goToSlide(currentSlideIndex - 1));
});
The Inside Scoop: Enjoyable Staff Tales & Hidden Gems
In the course of the challenge, we couldn’t resist sneaking in a little bit Easter egg—a hidden tribute to our developer that solely the keenest eyes will spot. We additionally made certain no staff members have been harmed within the making of this challenge (properly, apart from the sleepless nights perfecting these animations). All the course of was a artistic rollercoaster, stuffed with sudden discoveries, last-minute tweaks, and moments of pure pleasure as every thing lastly clicked collectively. On the finish of the day, Warhol Arts turned greater than only a web site—it turned a digital artwork experiment we’re extremely pleased with.
The Remaining Stroke
Warhol Arts isn’t only a web site—it’s an expertise. A tribute to a artistic insurgent. A playground for movement design. And a reminder that pushing boundaries is all the time price it.