JSFiddle - React, Tailwind, and code Playground

HTML

<!DOCTYPE html>
<html>
	<head>
		<title>Test</title>
		<style>
			body {
				margin: 0;
				padding: 0;
				transform-style: preserve-3d;
				transform: perspective(800px);
				background-color: #eee;
			}
		</style>
		<link rel="stylesheet" href="test.css">
	</head>
	<body>
		<div id="card-limiter"></div>
		<template id="card-template">
			<div class="x-slider">
				<div class="y-slider">
					<div class="z-slider">
						<div class="roller">
							<div class="card"></div>
						</div>
					</div>
				</div>
			</div>
		</template>
	</body>
</html>

CSS

.x-slider {
    position: absolute;
    width: 200px;
    height: 200px;
    transition-property: all;
    pointer-events: none;
}

.y-slider {
    position: absolute;
    width: 200px;
    height: 200px;
    transition-property: all;
}

.z-slider {
    position: absolute;
    width: 200px;
    height: 200px;
    transition-property: all;
}

.roller {
    position: relative;
    width: 200px;
    height: 200px;
    display: flex;
    justify-content: center;
    align-items: center;
}

.card {
    position: relative;
    width: 120px;
    height: 150px;
    background-color: #444;
    background-image: url('https://i.imgur.com/9LRlpwv.png');
    background-size: cover;
    transition: all 0.1s ease;
    border-radius: 8px;
    border: 5px solid #333;
    pointer-events: all;
}

.card:hover {
    border-color: #0f0;
}

JavaScript

class Card {
    constructor() {
        this.queue = [];
        this.inProgress = false;
        this.createView();
    }

    createView() {
        const x_slider = document.querySelector('#card-template').content.firstElementChild.cloneNode(true);
        const y_slider = x_slider.firstElementChild;
        const z_slider = y_slider.firstElementChild;
        const roller = z_slider.firstElementChild;
        const card = roller.firstElementChild;
        this.view = {x_slider, y_slider, z_slider, roller, card};
        document.body.insertBefore(x_slider, document.querySelector('#card-limiter'));
    }

    applyTransform({ x, y, z, rx, ry, rz }, ms, easing) {
        this.inProgress = true;
        const { x_slider, y_slider, z_slider, roller, card } = this.view;
        const duration = ms / 1000 + 's';
        if (x !== undefined) {
            x_slider.style.transform = `translateX(${x}px)`;
            x_slider.style.transitionDuration = duration;
            x_slider.style.transitionTimingFunction = easing;
        }
        if (y !== undefined) {
            y_slider.style.transform = `translateY(${y}px)`;
            y_slider.style.transitionDuration = duration;
            y_slider.style.transitionTimingFunction = easing;
        }
        if (z !== undefined) {
            z_slider.style.transform = `translateZ(${z}px)`;
            z_slider.style.transitionDuration = duration;
            z_slider.style.transitionTimingFunction = easing;
        }
        if (rx !== undefined || ry !== undefined || rz !== undefined) {
            let arr = [];
            if (rx !== undefined) {
                arr.push(`rotateX(${rx}rad)`);
            }
            if (ry !== undefined) {
                arr.push(`rotateY(${ry}rad)`);
            }
            if (rz !== undefined) {
                arr.push(`rotateZ(${rz}rad)`);
            }
            roller.style.transform = arr.join(' ');
            roller.style.transitionDuration = duration;
           ...