Howdy! I’m Andrea Biason, a Artistic Frontend Developer at Adoratorio Studio obsessed with volleyball, code, and issues in movement (together with GIFs, by the way in which!).
On this article, we’ll uncover how one can method a easy e-commerce touchdown web page and remodel it right into a extra interactive and interesting expertise for the person with the ultimate objective of accelerating conversions whereas additionally making the person journey extra partaking at such an important, but usually disregarded second.
“I’ve a pal who wants a touchdown web page for his merchandise. Are you in?”
After I bought referred to as for this undertaking, my first thought was that I didn’t need it to be the standard e-commerce website.
So, I requested the designer, “How a lot artistic freedom do I’ve?”.
Thankfully, the reply was “Do no matter you need” so I began desirous about what I might do to make the end result partaking.
“What if we add an animation to the CTA button once you click on it? The cart icon might seem…”
Uhm, really…no! An interplay on the ‘Add to cart’ button was the best answer, however I didn’t wish to go along with one thing already seen 1,000,000 occasions — I needed to strive creating one thing distinctive. The concept got here from desirous about two utterly separate and unrelated elements: a gallery and a mouse path on the cursor. I assumed it may be attention-grabbing to strive merging them, utilizing the numerous pictures we had obtainable to create a form of path from the product to the cart.
Such a interplay wouldn’t solely interact the person visually but additionally information their gaze in direction of the checkout course of and the checkout course of.
Let’s take a more in-depth take a look at the code.
The Markup
<part class="content material">
<div class="merchandise">
<ul class="products__list">
<li class="products__item" data-id="product-01" data-price="15" data-name="Product 01" data-cover="/pictures/product-01-cover.jpg">
<div class="products__images">
<img class="products__main-image" src="/pictures/product-01-cover.jpg" alt="Product 01">
<div class="products__gallery">
<img class="products__gallery-item" src="/pictures/galleries/product-01/01.jpg" alt="Product 01 gallery">
<img class="products__gallery-item" src="/pictures/galleries/product-01/02.jpg" alt="Product 01 gallery">
<img class="products__gallery-item" src="/pictures/galleries/product-01/03.jpg" alt="Product 01 gallery">
<img class="products__gallery-item" src="/pictures/galleries/product-01/04.jpg" alt="Product 01 gallery">
<img class="products__gallery-item" src="/pictures/galleries/product-01/05.jpg" alt="Product 01 gallery">
<img class="products__gallery-item" src="/pictures/product-01-cover.jpg" alt="Product 01 gallery">
</div>
</div>
<button kind="button" class="products__cta button">Add to cart</button>
</li>
<li>... </li>
<li>... </li>
<li>... </li>
<li>... </li>
<li>... </li>
</ul>
</div>
</part>
<apart class="cart">
<div class="cart__bg"></div>
<div class="cart__inner">
<div class="cart__inner-close">Shut</div>
<div class="cart__inner-bg"></div>
<div class="cart-items"></div>
<div class="cart-total cart-grid">
<div class="cart-total__inner">
<div class="cart-total__label">Complete:</div>
<div class="cart-total__amount">€ 0</div>
<div class="cart-total__taxes"> Supply charge and tax <br> calculated at checkout </div>
<a category="cart-total__checkout-btn button" href="#">Checkout</a>
</div>
</div>
</div>
</apart>
The HTML construction could be very easy. A CSS grid was created to rapidly arrange the product show, and inside every merchandise, a wrapper was created for the primary picture and gallery. The rationale for making a wrapper is to have a single factor with a set top, permitting all pictures inside to scale to 100% of the mother or father dimension, making responsive administration simpler as nicely.
At this level, the primary determination got here up: ought to I create all picture nodes instantly inside the markup or append the gallery pictures solely when the button is clicked? The primary method makes the HTML extra verbose and will increase the variety of nodes on the web page, whereas the second would require creating all pictures at runtime and including them to the node, delaying the animation’s begin and doubtlessly inflicting points with managing the queue for each processes.
I selected, due to this fact, to embody all pictures instantly within the HTML. This alternative additionally helped bypass a attainable extra problem: by retrieving all of the img nodes, I used to be capable of preload all pictures throughout the preliminary loading part whereas the preloader was nonetheless seen.
Alright, the HTML is prepared; it’s time to maneuver on to creating a category to handle the merchandise.
The “Merchandise” class
The Merchandise class has a quite simple construction and can primarily deal with:
- Figuring out the x and y coordinates of the cart within the header, which is the purpose in direction of which the animation will probably be directed;
- Including a click on listener on the CTAs to arrange the weather and begin the animation;
- Creating the animation timeline;
- Resetting the weather as soon as the animation is full.
export default class Merchandise {
constructor() {
this.merchandise = [...document.querySelectorAll('.products__item')];
this.ctas = [...document.querySelectorAll('.products__cta')];
this.cartButton = doc.querySelector('.cart-button');
this.cartButtonCoords = { x: 0, y: 0 };
this.currentProduct = null;
this.currentGallery = [];
this.otherProducts = [];
this.isTopRow = false;
this.init();
}
init() {
this.setCartButtonCoords();
this.ctas.forEach((cta, i) => {
cta.addEventListener('click on', () => {
this.currentProduct = this.merchandise[i];
this.otherProducts = this.merchandise.filter((prod, index) => index !== i);
this.currentGallery = [...this.currentProduct.querySelectorAll('.products__gallery-item')];
this.isTopRow = window.innerWidth > 768 && i < 3;
this.addToCart();
})
})
window.addEventListener('resize', debounce(() => {
this.setCartButtonCoords();
}))
}
setCartButtonCoords() {
const { x, y } = this.cartButton.getBoundingClientRect();
this.cartButtonCoords = { x, y };
}
...
Let’s rapidly break down the init methodology:
- The
this.setCartButtonCoords
methodology is named, which merely retrieves the x and y coordinates of the button within the header utilizinggetBoundingClientRect()
; - A click on listener is created for the CTAs, the place the animation will probably be executed. This methodology is simple: it merely defines the constructor values with the present merchandise to be animated, the opposite gadgets that must disappear, the energetic gallery to be animated, and the
this.isTopRow
subject, which will probably be used to outline the animation course; - A listener is created to observe resize occasions, resetting the cart coordinates each time the display dimension modifications. The debounce operate optimizes this by stopping the strategy from working on each pixel resize, as a substitute triggering it after a timeout on the finish of the browser’s resize operation.
Now, let’s check out the juicy half: the this.addToCart
methodology, the place the GSAP timeline is created.
The “Add to cart” animation
Let’s undergo the evolution of the timeline step-by-step, ranging from the fundamentals.
The very first step is to spotlight the chosen product and make the opposite gadgets disappear, then return all the things to the unique state as soon as the animation is full.
tl.to(this.otherProducts, {
scale: 0.8, autoAlpha: 0.05, period: 0.6, stagger: 0.04, ease: 'power2.out',
}, 'begin');
tl.to(this.currentProduct, {
scale: 1.05, period: 1, ease: 'power2.out',
}, 'begin+=0.7');
tl.to([this.currentProduct, this.otherProducts], {
scale: 1, autoAlpha: 1, period: 0.8, stagger: 0.03, ease: 'power2.out',
}, 'begin+=1.6');
The concept behind the animation is to maneuver the weather towards the cart coordinates, so step one within the timeline will probably be to tween the x and y coordinates of the gallery pictures.
tl.to(this.currentGallery, {
x: this.cartButtonCoords.x,
y: this.cartButtonCoords.y,
stagger: {
from: 'finish',
every: 0.04,
},
period: 1.8,
ease: 'power2.inOut'
});
We instantly face the primary drawback: the pictures are transferring downward as a substitute of upward, as we would anticipate. The reason being easy: we’re including the cart’s coordinates to the present coordinates of the picture.
The objective, due to this fact, will probably be to calculate the gap between the picture and the cart’s place, and subtract that distance throughout the tween. To do that, earlier than initializing the timeline, we retrieve the best and y coordinates of the present picture and subtract them from the cart’s coordinates.
const { y, proper} = this.currentGallery[0].getBoundingClientRect();
tl.to(this.currentGallery, {
x: this.cartButtonCoords.x - proper,
y: this.cartButtonCoords.y - y,
...
Now, as we are able to see, the pictures are transferring within the appropriate course in direction of the button.Let’s refine this primary step by including a fade-out impact to the pictures as they method their ultimate place, adjusting the size and autoAlpha
properties.
tl.to(this.currentGallery, {
x: this.cartButtonCoords.x - proper,
y: this.cartButtonCoords.y - y,
scale: 0,
autoAlpha: 0,
stagger: {
from: 'finish',
every: 0.04,
},
period: 1.8,
ease: 'power2.inOut'
}, 'begin');
Alright, this might already be a very good end result by adjusting the timeline period and easing, however the concept I had in thoughts was to create a extra elaborate path.
So, I considered splitting the timeline into two steps: a primary step the place the pictures would exit the body, and a second step the place they’d head in direction of the cart.
And that is the place GSAP keyframes got here to my rescue!
Step one is to return to the start of the animation and likewise retrieve the peak utilizing the getBoundingClientRect()
methodology. This worth is then used to maneuver the pictures by 150% at 40% of the animation, earlier than directing them in direction of the cart within the subsequent 60% of the animation.
tl.to(this.currentGallery, {
keyframes: {
'40%': {
y: top * 1.5,
scale: 0.8,
autoAlpha: 1,
},
'100%': {
x: this.cartButtonCoords.x - proper,
y: this.cartButtonCoords.y - y,
scale: 0,
autoAlpha: 0,
},
},
stagger: {
from: 'finish',
every: 0.04,
},
period: 1.8,
ease: 'power2.inOut',
}, 'begin');
Right here’s the ultimate end result, however at this level, one other problem arises: the animation works nicely for the highest row, however the impact is misplaced within the backside row.
So shut, but to date.
Okay, how will we deal with the animation for the underside rows? By reversing the course: as a substitute of transferring downward, they are going to take the other path, detaching upward first, after which transferring in direction of the cart.
So, let’s begin utilizing this.isTopRow
, which we created within the constructor, to outline whether or not the animation includes an merchandise from the highest row or the underside row.
Step one includes the transformOrigin
of the pictures.
gsap.set(this.currentGallery, { transformOrigin: this.isTopRow ? 'high proper' : 'backside left' });
Then, we proceed by modifying the course inside the keyframes, additionally retrieving the left place utilizing the preliminary getBoundingClientRect()
const { y, left, proper, top } = this.currentGallery[0].getBoundingClientRect();
...
keyframes: {
'40%': {
y: this.isTopRow ? top * 1.5 : -height * 1.5,
scale: this.isTopRow ? 0.8 : 0.5,
autoAlpha: 1,
},
'100%': {
x: this.isTopRow ? this.cartButtonCoords.x - proper : this.cartButtonCoords.x - left,
y: this.isTopRow ? this.cartButtonCoords.y - y : this.cartButtonCoords.y - y - top,
scale: 0,
autoAlpha: 0,
},
},
Okay, we’re virtually there! There’s nonetheless a small imperfection within the animation of the underside row, attributable to the transformOrigin
we simply set in the beginning of the timeline.
To visually appropriate the ultimate level, we’ll subtract an arbitrary worth from the vacation spot of the animation, equivalent to the dimensions of the cart merchandise depend badge.
'100%': {
x: this.isTopRow ? this.cartButtonCoords.x - proper : this.cartButtonCoords.x - left - 12, // eradicating half button width
y: this.isTopRow ? this.cartButtonCoords.y - y : this.cartButtonCoords.y - y - top + 25, // including full button top
scale: 0,
autoAlpha: 0,
},
Right here’s the ultimate end result:
Now, let’s handle resetting the animation on the finish of the timeline:
onComplete: () => {
gsap.set(this.currentGallery, { scale: 1, autoAlpha: 1, y: 0, x: 0 });
gsap.set(gallery, { autoAlpha: 0 });
this.resetAnimation()
},
We merely return the weather of the gallery, which had been simply animated, to their authentic place (which is strictly overlapping with the primary picture that continues to be seen, so no variations are noticeable), set the opacity to 0, and execute the strategy that clears the objects within the constructor.
resetAnimation() {
this.currentProduct = null;
this.currentGallery = [];
this.otherProducts = [];
}
The reset operate won’t even should be executed, since each time the press occasion is triggered on the CTA, the array is rewritten. Nevertheless, it’s nonetheless preferable to maintain the arrays empty as soon as we now not must work with the weather contained in them.
Okay, are we carried out? I’d say not but, we nonetheless must handle the cart!
Let’s not go away issues unfinished.
The Cart class was divided into two logical blocks throughout growth: the primary one solely includes the acquisition state of affairs, and the second focuses solely on the logic for the doorway and exit animations of the sidebar.
Let’s begin with the product administration state of affairs:
addItemToCart(el) {
const { id, value, title, cowl } = el.dataset;
const index = this.cartItems.findIndex((el) => el.id === id);
if (index < 0) {
const newItem = { id, value, title, cowl, amount: 1 };
this.cartItems.push(newItem);
const newCartItem = this.appendItem(newItem);
this.cartItemsList.append(newCartItem);
} else this.cartItems[index].amount += 1;
this.updateCart();
}
The tactic for including a product to the cart could be very easy, and right here too it divides the logic into two situations:
- The clicked CTA is for a new product;
- The clicked CTA is for a product already within the cart.
The this.cartItems array within the constructor represents the checklist of all gadgets added to the cart, and is due to this fact used inside the methodology to modify between the attainable situations. If the product isn’t already within the cart, it’s pushed into the this.cartItems array, and the HTML node is created by means of the this.appendItem methodology. If the product is already within the cart, it’s merely retrieved by its index, and the amount is up to date.
Let’s rapidly undergo the this.appendItem methodology:
appendItem(merchandise) {
const cartItem = doc.createElement('div');
cartItem.classList.add('cart-item', 'cart-grid');
cartItem.innerHTML = `
<img class="cart-item__img" src="${merchandise.cowl}" alt="${merchandise.title}">
<div class="cart-item__details">
<span class="cart-item__details-title">${merchandise.title}</span>
<button class="cart-item__remove-btn">Take away</button>
<div class="cart-item__details-wrap">
<span class="cart-item__details-label">Amount:</span>
<div class="cart-item__details-actions">
<button class="cart-item__minus-button">-</button>
<span class="cart-item__quantity">${merchandise.amount}</span>
<button class="cart-item__plus-button">+</button>
</div>
<span class="cart-item__details-price">€ ${merchandise.value}</span>
</div>
</div>
`;
const removeButton = cartItem.querySelector('.cart-item__remove-btn');
const plusButton = cartItem.querySelector('.cart-item__plus-button');
const minusButton = cartItem.querySelector('.cart-item__minus-button');
removeButton.addEventListener('click on', () => this.removeItemFromCart(merchandise.id));
plusButton.addEventListener('click on', () => this.updateQuantity(merchandise.id, 1));
minusButton.addEventListener('click on', () => this.updateQuantity(merchandise.id, -1));
return cartItem;
)
Along with including the HTML node, I additionally arrange all of the listeners for the varied buttons that make up the UI, linking them to their respective strategies:
- The “Take away” button will execute the
this.removeItemFromCart(merchandise.id)
methodology to take away the thing from the array of energetic merchandise and the HTML node. - The “+” and “-” buttons modify the amount of merchandise within the cart and execute the this.updateQuantity(merchandise.id, 1 / -1) methodology, passing as a parameter the amount so as to add or take away.
On the finish of every cart modification (addition/elimination/amount change), I’ve arrange an replace methodology to switch the checkout whole.
updateCart() {
const cartElementsQuantities = [...document.querySelectorAll('.cart-item__quantity')];
this.cartButtonNumber.innerHTML = Object.values(this.cartItems).size;
let cartAmount = 0;
Object.values(this.cartItems).forEach((merchandise, i) => {
cartElementsQuantities[i].innerHTML = merchandise.amount;
cartAmount+= merchandise.value * merchandise.amount
})
this.cartTotal.innerHTML = `€ ${cartAmount}`;
}
This code was created for primary performance and would should be expanded to work correctly with an e-commerce website. In my case, having chosen the Shopify platform, I used the shopify-buy library to handle the APIs and sync the cart checkout with the ultimate checkout on the platform, however every platform has its personal APIs to deal with this.
One other attainable implementation, barely extra complicated however positively extra user-friendly, could be to handle the merchandise added to the cart by saving them in LocalStorage, making certain they continue to be in reminiscence even when the person reloads the web page.
The ultimate step to finish the product addition to the cart will probably be, due to this fact, to execute the addItemToCart
methodology inside the timeline created earlier.
tl.add(() => {
Cart.addItemToCart(this.currentProduct);
}, 'begin+=0.6');
On this manner, throughout the animation of the pictures, the present product will even be pushed into the cart.
And why not animate the button with the variety of gadgets at this level?
Let’s convey it dwelling.
Throughout the init methodology of the Cart
class, we initialize the button that will probably be animated setting parts to 0 scale.
Then we merely add, nonetheless inside the primary cart addition timeline, the this.cartButtonAnimationEnter
methodology, however provided that the present variety of merchandise within the cart is 0.
tl.add(() => {
if (Cart.cartItems.size === 0) Cart.cartButtonAnimationEnter();
Cart.addItemToCart(this.currentProduct);
}, 'begin+=0.6');
cartButtonAnimationEnter() {
const tl = gsap.timeline();
tl.addLabel('begin');
tl.to(this.cartButtonLabel, { x: -35, period: 0.4, ease: 'power2.out' }, 'begin');
tl.to([this.cartButtonNumber, this.cartButtonBg], {
scale: 1, stagger: 0.1, period: 0.8, ease: 'elastic.out(1.3, 0.9)',
}, 'begin');
return tl;
}
And now, the ultimate half, probably the most juicy one, which includes the doorway and exit animation of the cart.
So let it out and let it in.
Nonetheless inside the init methodology of the Cart
class, we’ll handle two basic steps for the complete circulation.
Step one is to execute the setup capabilities for the weather to animate, each cart button and cart opening animation.
The second step is to handle occasion listeners for enter and go away animations, primarily based on cart and shut buttons interactions:
init() {
this.cartButtonAnimationSetup();
this.cartAnimationSetup();
this.cartButton.addEventListener('click on', () => {
if (this.isAnimating) return;
doc.physique.classList.add('locked');
this.isAnimating = true;
this.cartAnimationEnter().then(() => {
this.cartOpened = true;
this.isAnimating = false;
})
})
this.cartClose.addEventListener('click on', () => {
if (this.isAnimating) return;
doc.physique.classList.take away('locked');
this.isAnimating = true;
this.cartAnimationLeave().then(() => {
this.cartOpened = false;
this.isAnimating = false;
})
})
}
Let’s rapidly analyze:
this.isAnimating
is used to stop the overlap of the 2 timelines (this can be a stylistic alternative, not a compulsory one; the choice is to handle the factor queues with thekillTweensOf
methodology from GSAP). If an animation is in progress, its reverse can’t be triggered till it’s accomplished;- The locked class is added to the physique to block scrolling;
- The doorway/exit animation is triggered, after which the values
this.isAnimating
andthis.cartOpened
are set.
One final small observe on the doorway animation:
cartAnimationEnter() {
this.animatingElements.gadgets = [...this.cart.querySelectorAll('.cart-item')];
if (this.animatingElements.gadgets.size > 0) gsap.set(this.animatingElements.gadgets, { x: 30, autoAlpha: 0 });
const tl = gsap.timeline({
onStart: () => gsap.set(this.cart, { xPercent: 0 })
});
tl.addLabel('begin');
tl.to([this.animatingElements.bg, this.animatingElements.innerBg], {
xPercent: 0, stagger: 0.1, period: 2.2, ease: 'expo.inOut',
}, 'begin');
tl.to(this.animatingElements.shut, {
x: 0, autoAlpha: 1, stagger: 0.1, period: 1, ease: 'power2.out',
}, 'begin+=1.3');
if (this.animatingElements.gadgets.size > 0) {
tl.to(this.animatingElements.gadgets, {
x: 0, autoAlpha: 1, stagger: 0.1, period: 1, ease: 'power2.out',
}, 'begin+=1.4');
}
if (this.animatingElements.noProds) {
tl.to(this.animatingElements.noProds, {
x: 0, autoAlpha: 1, stagger: 0.1, period: 1, ease: 'power2.out',
}, 'begin+=1.4');
}
tl.to(this.animatingElements.whole, {
scale: 1, autoAlpha: 1, stagger: 0.1, period: 1, ease: 'power2.out',
}, 'begin+=1.6');
return tl;
};
this.animatingElements.gadgets
isn’t outlined inside the this.cartAnimationSetup
operate as a result of the variety of parts modifications every time they’re added by the animation, whereas that is solely referred to as throughout the initialization of the Cart class.
If we didn’t set the weather each time we run the doorway animation, this.animatingElements.gadgets would all the time be an empty array, and due to this fact, we might by no means see the gadgets added to the cart.
The go away animation merely repositions the weather exterior of the structure:
cartAnimationLeave() {
const tl = gsap.timeline({
onComplete: () => gsap.set(this.cart, { xPercent: 100 })
});
tl.addLabel('begin');
tl.to([this.animatingElements.bg, this.animatingElements.innerBg], {
xPercent: 110, stagger: 0.1, period: 1.5, ease: 'expo.inOut',
}, 'begin');
if (this.animatingElements.gadgets.size > 0) {
tl.to(this.animatingElements.gadgets, {
x: 30, autoAlpha: 0, stagger: 0.1, period: 0.8, ease: 'power2.out',
}, 'begin');
}
if (this.animatingElements.noProds) {
tl.to(this.animatingElements.noProds, {
x: 30, autoAlpha: 0, stagger: 0.1, period: 0.8, ease: 'power2.out',
}, 'begin');
}
tl.to(this.animatingElements.shut, {
x: 30, autoAlpha: 0, stagger: 0.1, period: 0.8, ease: 'power2.out',
}, 'begin');
tl.to(this.animatingElements.whole, {
scale: 0.9, autoAlpha: 0, stagger: 0.1, period: 0.8, ease: 'power2.out',
}, 'begin');
return tl;
}
And right here is the ultimate end result with the cart animation!
Ah, possibly you would possibly’ve caught on however I forgot to say that I’m an enormous fan of The Workplace and that…