Developing the Sliced Image Hover Effect: Quai Network’s Coding Execution

I have a serious weakness for futuristic interfaces. And hover effects. Naturally, when I stumbled upon the website of Quai Network made by Unseen Studio, I totally fell in love with the whole design and interactive effects. Besides the amazing graphics and interactivity, I found their hover animation to be a really nice one to rebuild using a dynamic clip path effect: Please note that the effect we’ll be constructing in this tutorial won’t be an exact replica, but rather a similar animation that emulates the motion found on their website. For the animations, we’ll use GSAP by GreenSock. Let’s get started!

The Markup

When we look at the effect on Quai Network, we can see that several slices of the same image move into view once we hover a grid item. The slices are actually super tall and the image inside is repeated. There’s also a cool letter shuffling effect. We will make a simpler version, by using slices that are as high as the grid item. What we need is some sort of wrapper where we insert all the images that will have a clip-path applied to them, creating those slices. Let’s set up a basic structure that we’ll then augment dynamically in our JavaScript. This structure will also serve as a fallback:

<article class=”card”>
<div class=”card__img” style=”background-image:url(img/img1.jpg)”></div>
<span class=”card__date” data-splitting>02/18/2074</span>
<h2 class=”card__title” data-splitting>Code CR-4519: Anomaly Detection in Array</h2>
<a href=”#” class=”card__link” data-splitting>Read the article</a>

The texts we’ll want to shuffle, will have the data-splitting attribute as we’ll use Splitting.js to help us with that by separating all words into letter spans. The card__img element is our container for all the slices we’ll generate. Additionally, to achieve some sort of “reveal” effect, we will need another wrapper element for all those slices. Something like this:

<div class=”card__img” >
<div class=”card__img-wrap”>
<div class=”card__img-inner”></div>
<div class=”card__img-inner”></div>
<div class=”card__img-inner”></div>
<div class=”card__img-inner”></div>
<div class=”card__img-inner”></div>

Where each card__img-inner will have a clip-path applied to and also the same background image set. The card__img-wrap element will be translated initially and then moved into view. By translating the inner slices in the opposite direction, we can create a sort of “unreveal” animation. Let’s move on to the styles.


The first thing that’s important for our effect is the style of the card:

.card {
display: grid;
grid-template-rows: auto 1fr auto;
cursor: pointer;
position: relative;
min-height: 60vh;
padding: 4vw;
border-bottom: 1px solid var(–color-border);
overflow: hidden;

Using a grid layout, we can position the elements easily. You could also use flexbox here. Now, let’s define the styles for the crucial part, the slices and all involved elements. We need to make sure that everything is nicely stackable, so we use absolute positioning for all of them. We don’t want the pieces to fly out of the card, so we make sure that overflows are hidden on the right elements. To make sure we don’t see any nasty gaps caused by anti-aliasing when using clip-path, we apply a little trick. For each slice bigger than one, we add a pixel to the width and pull the element back by setting the left accordingly. This we will do dynamically. So, for example, the second slice will have a width of 100% + 1px and a left of -1px. This will make it move on top of the first slice, hiding any potential gaps! While we will set the lefts in our JavaScript, we can take care of the width in the CSS by passing over two variables that will be defined in our script, –columns and –rows. If we stack the slices horizontally, we’ll set the –rows variable, which will cause the height to adjust. If we stack the slices vertically, we’ll set the –columns variable, which in turn will cause the width to adjust.

.card__img, .card__img-wrap, .card__img-inner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;

.card__img, .card__img-inner {
background-size: cover;
background-position: 50% 50%;

.card__img, .card__img-wrap {
overflow: hidden;

.card__img {
z-index: -1;
pointer-events: none;
–columns: 0;
–rows: 0;

.js .card__img {
opacity: 0;
background-image: none !important;

.card__img-inner {
filter: brightness(0.6);
width: calc(100% + (var(–columns) – 1) * 1px);
height: calc(100% + (var(–rows) – 1) * 1px);

Last, but not least, let’s style the remaining elements:

.card__date {
display: flex;
align-content: center;
align-items: center;
line-height: 1;
position: relative;

.card__date::before {
content: ”;
width: 15px;
height: 15px;
border: 1px solid var(–color-link);
background: var(–color-bg-date);
margin: 0 10px 4px 0;

.card__title {
font-weight: 400;
font-size: clamp(1.5rem,5vw,2.5rem);

.card__link {
position: relative;

.card__link::before {
content: ‘+’;
margin-right: 10px;

The JavaScript

We will need two JavaScript files: card.js, which will contain our Card class, and index.js, which will initialize our cards.

Creating the Card class

Our Card class will handle the functionality of the card component. We’ll start by defining some properties and a constructor in our card.js file.

// Class representing a Card
export class Card {
// Initialize DOM and style related properties
DOM = {
// main DOM element
el: null,
// .card__img element
img: null,
// .card__img-wrap element (dynamically created in the layout function)
imgWrap: null,
// .card__img-inner “slice” elements (dynamically created in the layout function)
slices: null,
// .card__date element
date: null,
// .card__title element
title: null,
// .card__link element
link: null,

// Card image url

// Settings
settings = {
// vertical || horizontal alignment
orientation: ‘vertical’,
// Total number of slices for the inner images (clip paths)
slicesTotal: 5,
// Animation values
animation: {
duration: 0.5,
ease: ‘power3.inOut’

* Sets up the necessary elements, data, and event listeners for a Card instance.
* @param {HTMLElement} DOM_el – The DOM element that represents the card.
* @param {Object} options – The options for customizing the card. These options will override the default settings.
constructor(DOM_el, options) {
// Merge settings and options.
this.settings = Object.assign({}, this.settings, options);

this.DOM.el = DOM_el;
this.DOM.img = this.DOM.el.querySelector(‘.card__img’);

Source link

Leave a Reply