This venture’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 artistic director at BL/S®, to take us behind the scenes. On this deep dive, he breaks down how the venture went from a enjoyable little Dribbble shot to a full-blown digital expertise—overlaying every thing from customized animations and efficiency methods to the inventive 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 check. Growth—Warhol Arts changed into a full-blown digital wonderland. What began as a aspect venture developed into one thing a lot greater: an interactive tribute to the king of pop artwork, Andy Warhol. Daring, surprising, and stuffed with dynamic GSAP magic.

The Massive Concept 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 will we channel that power into an internet 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 persona of Warhol Arts. Animation wasn’t simply an afterthought—it was a part of the idea from day one. We needed the location to really feel alive, reactive, and surprising, like a digital extension of Warhol’s power. On this part, we’ll stroll by 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 rigorously 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 straight 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’ll be able to see it in motion:
The code appears as follows:
Elvis textual content strikes primarily based on the cursor on the left
Because the cursor strikes, we seize its X-axis coordinates and dynamically regulate the font dimension utilizing a GSAP animation, guaranteeing easy and optimized transitions. Moreover, we carried out logic to disable the mouse occasion listener outdoors this part, retaining the impact contained and environment friendly.
That is it the way it appears:


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 carried out utilizing Webflow animations: Webflow Interactions > Whereas Scrolling in View.




Footer path impact
The path impact can be carried out 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 are not any delays or lags.


showNextImage() {
++this.zIndexVal;
this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
const img = this.pictures[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.internal, {
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 factor
Motion of numbers relying on the cursor, carried out utilizing Webflow animations.



Textual content crammed by a masks whereas scrolling
Textual content masks filling utilizing the ScrollTrigger plugin supplied by GSAP, which permits us to work together with the location by 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
});
}
Common textual content look throughout the location utilizing GSAP
All through our web site, textual content animations are positioned and carried out in a method that they set off when scrolling reaches a particular factor (utilizing the ScrollTrigger plugin). This strategy makes it extra optimized, because the animation solely triggers when wanted, fairly 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 factor 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 through 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
Throughout the venture, we couldn’t resist sneaking in somewhat Easter egg—a hidden tribute to our developer that solely the keenest eyes will spot. We additionally made positive no workforce members have been harmed within the making of this venture (properly, aside from the sleepless nights perfecting these animations). All the course of was a inventive rollercoaster, crammed with surprising 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 happy with.
The Remaining Stroke
Warhol Arts isn’t only a web site—it’s an expertise. A tribute to a inventive insurgent. A playground for movement design. And a reminder that pushing boundaries is at all times value it.