Part of a project I’m working on involves some animation of “cards” sliding in and out of existence. This trinity of HTML/CSS/JS serves as a basis for that functionality.
The illusion is that there is an endless (at least until integer overflow anyway) number of rectangles that will scroll either forward or backward.
The truth is that there are only 3 such rectangles. Theoretically only 2 are needed, but that comes with the cost of more complexity in the code. When a scroll is happening, there is one rectangle that is scrolling out, then there is another one scrolling in to take its place. Doing it once is easy with CSS transform. However, to continue to do it over and over again seamlessly requires some trickery.
The trick is that, once the transform animation to replace the center rectangle with the new rectangle completes, we need to immediately shift the rectangles back to be ready for the next scroll. This is done by the reset()
function. The sequence looks like this:
- Render the new rectangle (off screen).
- Scroll the center rectangle out.
- Scroll the new rectangle in.
- Render the center rectangle (off screen) to match the new rectangle.
- Reset the position of the rectangles so that the center rectangle is again at the center. Because the content of the rerendered center rectangle now matches that of the new rectangle, this should look seamless.
This is done by the functions slideRightWithCount
and slideLeftWithCount
. Adding the slideRight
or slideLeft
class to the rectangles will animate the sliding for 0.3s. At the same time, set a timer for 300ms (0.3s) to then call reset()
which positions the rectangles to be ready for another scroll. There is some 🤞 going on assuming that the transform animation is done by the time the timer expires and reset()
is called (preferrably at the same time). If for some reason the timer expires earlier than the full transform is complete, there can be some flickering (easily tested by either increasing the transition duration and/or decreasing the timer expiry).
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rectangle Animation</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="rects">
<div class="container">
<div class="rectangle" id="rectangle0"></div>
<div class="rectangle" id="rectangle1">1</div>
<div class="rectangle" id="rectangle2"></div>
</div>
</div>
<div id="button-panel">
<div>
<button onclick="slideRightWithCount()">BACK</button>
</div>
<div>
<button onclick="slideLeftWithCount()">FORWARD</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
STYLES.CSS
body {
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
#rects {
justify-content: center;
align-items: center;
width: 100%;
height: 200px;
display: flex;
}
.container {
position: absolute;
width: 200px;
height: 100px;
overflow: hidden;
}
.rectangle {
position: absolute;
width: 100%;
height: 100%;
text-align: center;
padding: 1rem;
border: 10px solid white;
box-sizing: border-box;
background-color: antiquewhite;
}
#rectangle0 {
left: -200px;
}
#rectangle2 {
left: 200px;
}
.slide-left {
transition: transform 0.3s;
transform: translate(-200px)
}
.slide-right {
transition: transform 0.3s;
transform: translate(200px)
}
#button-panel {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: .5rem;
}
#button-panel button {
width: 7rem;
}
SCRIPT.JS
const RECT_IDS = [
"rectangle0", "rectangle1", "rectangle2"
]
var COUNTER = 1
function slideLeft() {
const rects = RECT_IDS.map((id) => document.getElementById(id))
rects.forEach((elem) => {
elem.classList.add("slide-left")
elem.classList.remove("slide-right")
})
};
function reset() {
const rects = RECT_IDS.map((id) => document.getElementById(id))
document.getElementById("rectangle1").innerText = COUNTER
rects.forEach((elem) => {
elem.classList.remove("slide-left")
elem.classList.remove("slide-right")
})
}
function slideRight() {
const rects = RECT_IDS.map((id) => document.getElementById(id))
rects.forEach((elem) => {
elem.classList.add("slide-right")
elem.classList.remove("slide-left")
})
};
function slideLeftWithCount() {
COUNTER = COUNTER + 1
document.getElementById("rectangle2").innerText = COUNTER
slideLeft()
setTimeout(() => {
reset()
}, 300)
}
function slideRightWithCount() {
COUNTER = COUNTER - 1
document.getElementById("rectangle0").innerText = COUNTER
slideRight()
setTimeout(() => {
reset()
}, 300)
}
The next challenge is to make this work with React.js. 😵💫