In this blog post, I will show you how I have created animated columns of tiles that smoothly transition their background images. This technique not only adds depth and interest to your web page but also provides an elegant way to showcase multiple images or content pieces in a limited space.
A basic understanding of HTML, CSS, and JavaScript will be sufficient to follow along. Let's dive in and explore how to create these visually appealing, animated columns.
Allow me to show you!
Content
- What are we building?
- Painting Our HTML with Tailwind (Utility-first CSS framework).
- Write some JavaScript to make it animate.
- Summary and code-sample.
What are we building?
We are building a row of interactive columns. Each column will hold two tiles (an upper tile and a lower tile). When the user hovers over a tile, it will expand its height to take up all the space of its parent column. Then, a new background image will fade in—this could be an image of a product, for example.
Additionally, I will wrap all the columns inside a responsive slider (using Swiper) to control the number of columns displayed on different screen sizes.
Painting Our HTML with Tailwind.
To get started, we'll need some data to work with. This data will be used to create a loop and dynamically generate the needed HTML. Here's the JavaScript columns
array I'll be using for this purpose:
const columns = [
...
{
tiles: [
{
positionClass: 'top-0',
tileClass: 'top-tile',
tileMaskClass: 'top-tile_mask',
bgImageExpr: "bg-[url('/bmw.png')]",
bgColor: 'bg-black-dark',
logo: 'bmw-logo.svg'
},
{
positionClass: 'bottom-0',
tileClass: 'bottom-tile',
tileMaskClass: 'bottom-tile_mask',
bgImageExpr: "bg-[url('/chevrolet.png')]",
bgColor: 'bg-black-light',
logo: 'chevrolet-logo.svg'
}
]
}
...
]
Every tile
object within columns
array has the necessary props for styling:
-
positionClass
prop hastop-0
andbottom-0
tailwind classes for positioning the tile to the top or bottom. -
tileClass
prop hastop-tile
class orbottom-tile
class. They are needed by JavaScript Dom selectors. -
tileMaskClass
prop hastop-tile_mask
class orbottom-tile_mask
class. They are also needed by JavaScript Dom selectors. -
bgImageExpr
prop will set the background-image of that tile using the tailwind exprbg-[url('')]
. -
bgColor
prop provides a background-color for the tile's mask. -
logo
prop provides the logo svg for the tile's mask.
Now we can map the columns
array to HTML:
<div class="swiper w-full h-[27.5rem] max-w-[13.75rem] md:max-w-[41.25rem] lg:max-w-[55rem] xlg:max-w-[68.75rem]">
<div class="swiper-wrapper">
{ columns.map( column => {
return <div class="swiper-slide">
<div class="single-column relative w-[13.75rem] h-[27.5rem] overflow-hidden">
{ column.tiles.map( tile => {
return <div class:list={[
tile.tileClass,
`w-full h-1/2 absolute ${tile.positionClass} left-0`,
`${tile.bgImageExpr}`,
"bg-center border border-black-light transition-[height] duration-300 ease-out"
]}>
<div class:list={[
tile.tileMaskClass,
"w-full h-full flex items-center justify-center",
tile.bgColor,
"bg-[length:7.5rem] bg-center bg-no-repeat transition duration-300 ease-out"
]} style={`background-image: url('/${tile.logo}');`}>
</div>
</div>
}) }
</div>
</div>
}) }
</div>
</div>
Write some JavaScript to make it animate.
When the user interacts with the tiles we want to modify their appearance. To achieve that we will attach event listeners to elements .top-tile_mask
and .bottom-tile_mask
.
In Case of .top-tile_mask
element:
- On
mouseenter
, I will set the opacity of the top tile's mask to 0 (making it invisible), change the z-index of its parent (.top-tile
element) to 10, and also set that parent's height to 100%. - On
mouseout
, I will set the opacity of the mask back to 1 (making it visible again) and reset its parent's height to 220px.
In Case of .bottom-tile_mask
element:
- On
mouseenter
, I will set the opacity of the bottom tile's mask to 0, unset the z-index of the.top-tile
element, and set the.bottom-tile_mask
parent's height to 100%. - On
mouseout
, I will set the opacity of the bottom tile's mask back to 1 and reset its parent's height to 220px.
Here's the JavaScript code that implements this functionality:
this.$el.querySelectorAll<HTMLElement>('.single-column').forEach( column => {
const topTileMask = column.querySelector<HTMLElement>('.top-tile_mask')!
const bottomTileMask = column.querySelector<HTMLElement>('.bottom-tile_mask')!
if(topTileMask && topTileMask.parentNode) {
topTileMask.addEventListener('mouseenter', () => {
topTileMask.style.opacity = '0';
(topTileMask.parentNode as HTMLElement).style.zIndex = '10';
(topTileMask.parentNode as HTMLElement).style.height = '100%';
})
topTileMask.addEventListener('mouseout', () => {
topTileMask.style.opacity = '1';
(topTileMask.parentNode as HTMLElement).style.height = '220px';
})
}
if(bottomTileMask && bottomTileMask.parentNode) {
bottomTileMask.addEventListener('mouseenter', () => {
bottomTileMask.style.opacity = '0';
(topTileMask.parentNode as HTMLElement).style.zIndex = 'unset';
(bottomTileMask.parentNode as HTMLElement).style.height = '100%';
})
bottomTileMask.addEventListener('mouseout', () => {
bottomTileMask.style.opacity = '1';
(bottomTileMask.parentNode as HTMLElement).style.height = '220px';
})
}
})
Note: this.$el
is there because I'm using Alpinejs to wrap all the code inside a component. $el
is a ref to the root element.
Summary
With our HTML structure and JavaScript in place, the animated columns are now fully functional. When users interact with the tiles, the animations will trigger, smoothly transitioning the background images and adjusting the tile sizes. The right styles will be dynamically applied to the elements, creating the visually appealing effect we set out to achieve.
🙏 Your feedback is much appreciated
Top comments (0)