Infinite Sliding Cards

HTML/CSS, Javascript, programming

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. 😵‍💫

Flipping Off CSS and JavaScript

HTML/CSS, Javascript, programming

I asked CoPilot in Microsoft Edge to help me get started with something in a Web page to allow me flip between cards:

You are an expert in Javascript and CSS.

Generate the code in HTML, Javascript, and CSS to animate flipping a picture along the X-axis and then revealing a different picture.

Honestly I didn’t know how well that was going to turn out. Surprisingly, it came out pretty well; It generated these:

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image Flip Animation</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="flip-container" onclick="flipImage()">
        <div class="flipper">
            <img src="image1.jpg" alt="Image 1" class="front">
            <img src="image2.jpg" alt="Image 2" class="back">
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

CSS

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f0f0f0;
    margin: 0;
}

.flip-container {
    perspective: 1000px;
}

.flipper {
    position: relative;
    width: 300px;
    height: 200px;
    transform-style: preserve-3d;
    transition: transform 0.6s;
}

.flip-container.flipped .flipper {
    transform: rotateX(180deg);
}

.front, .back {
    position: absolute;
    width: 100%;
    height: 100%;
    backface-visibility: hidden;
}

.front {
    z-index: 2;
    transform: rotateX(0deg);
}

.back {
    transform: rotateX(180deg);
}

JavaScript:

function flipImage() {
    const container = document.querySelector('.flip-container');
    container.classList.toggle('flipped');
}

I plugged in two JPGs I had lying around, and it did work quite well.


With that, I tweaked it to fit more what I had in mind.

HTML:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image Flip Animation</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="flip-container">
        <div class="flip-element">
            <div class="textcard-container front">
                <div class="title">FRONT</div>
                <div class="body">This is the front card</div>
            </div>
            <div class="textcard-container back">
                <div class="title">BACK</div>
                <div class="body">
                    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque imperdiet libero eu neque facilisis, ac pretium nisi dignissim. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla.
                </div>
            </div>
        </div>
    </div>
    <div id="flipper">
        <button onclick="flipImage()">Flip</button>
    </div>
    <script src="script.js"></script>
</body>
</html>

CSS

.flip-container {
    perspective: 1000px;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 80vh;
    background-color: #f0f0f0;
    margin: 0;    
}

.flip-element {
    position: relative;
    width: 300px;
    height: 200px;
    transform-style: preserve-3d;
    transition: transform 0.3s;
}

.flip-container.flipped .flip-element {
    transform: rotateX(180deg);
}

.front, .back {
    position: absolute;
    width: 100%;
    height: 100%;
    backface-visibility: hidden;
}

.front {
    z-index: 2;
    transform: rotateX(0deg);
    background-color: cornsilk;
}

.back {
    transform: rotateX(180deg);
    background-color: white;
}

.textcard-container {
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    align-items: center;
    text-align: center;
}

.textcard-container .title {
    justify-self: flex-start;
    width: 100%;
    box-sizing: border-box;
}

.textcard-container .body {
    justify-self: flex-end;
    box-sizing: border-box;
    padding: 15px;
    overflow: scroll;
}

.textcard-container .body:focus {
    outline-color: cornsilk;
}

#flipper {
    width: 100%;
    text-align: center;
    margin-top: 15px;
}

The JavaScript remains unchanged for now.

Looking at the various pieces, here are the notes:

Stacking Two Elements

The first thing is stacking the two cards with the “front” card on top of the “back” card. This is done by:

  1. Use the same dimensions for two DIVs (cards) inside a parent DIV (container).
  2. Add transform-style: preserve-3d for the container so that whatever animation we apply will stay.
  3. Add a transition: transform 0.3s (or however long desired for the animation sequence) to the container as well.
  4. Use absolute positioning for the cards so that they overlap.
  5. Give the front card a z-index of 2 (anything higher than that of the back card).
  6. Set the backface-visibility property to hidden. This ensures that cards flipped will be hidden.
  7. Rotate the back card by 180 degrees. This makes the back card invisible for now. It’s strictly not necessary since it’s obscured by the front card. However, this will be important when we do the flip.

Flip Animation

Set a CSS rule “flipped” that, when applied to the container, will rotate both child cards 180 degrees. This will:

  1. Flip the front card 180 degrees, and because we have backface-visibility: hidden, the front card will now be invisible (even though its z-index is still higher than that of the back card).
  2. Flip the back card 180 degrees. Since the back card was initially flipped 180 degrees, another 180 degrees now flips it to be front-facing. And since it’s now no longer flipped, it will be visible!
  3. In summary, when we flip both cards, we make the front card invisible and the back card visible.
    • If we didn’t need to animate, then theoretically we could leave the back card alone (visible) and just flip the front card.
    • However, because we ARE animating the flip, leaving the back card stationary will have it show through as the front card rotates in and out of visibility. Therefore, flipping both cards will give the smooth illusion that we are flipping a two-faced card with the front and back cards’ contents. Sneaky, isn’t it?

Now it just relies on toggling the CSS class “flipped” on the container. And that’s where the JavaScript comes in.

I didn’t know that JavaScript now can toggle a class for an element easily using the {element}.classList.toggle({class name}) function. I used to have to split the class list into an array and then iterate through the array to either add or skip and then setting the class list to the resulting list. This was a nice surprise.

Perspective

Setting perspective: 1000px on the container adds a nice touch that, when flipping, there’s a small 3-D effect. Though it’s something I’d miss if I blinked.