<main></main>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
perspective: 800px;
overflow: hidden;
display: grid;
place-items: center;
background: linear-gradient(#3a4149, #111722);
}
main {
position: relative;
transform-style: preserve-3d;
width: 50vmin;
height: 75vmin;
transition: all 500ms ease;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
article {
position: absolute;
inset: 0;
-webkit-box-reflect: below 50px
linear-gradient(transparent, rgba(255, 255, 255, 0.15));
button {
border: 0;
outline: 0;
cursor: pointer;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 16px;
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 200ms ease;
&:focus-visible {
box-shadow: 0 0 0 3px cyan;
}
// fallback
&:focus {
box-shadow: 0 0 0 3px cyan;
}
&:focus:not(:focus-visible) {
box-shadow: none;
}
&:hover,
&:focus-visible {
transform: scale(1.03) translateY(-4%);
}
}
}
View Compiled
/**
* Initially the elements are positioned in front of the camera.
* We define here the transformations to position them in the scene.
* When clicking on an item, we apply the inversed transformation but on the main container to "zoom" on the item
*/
const articles = new Map([
["6392322", { tx: "-90%", tz: "-70vmin", ry: "60deg" }],
["1761279", { tz: "-110vmin" }],
["1679772", { tx: "90%", tz: "-70vmin", ry: "-60deg" }]
]);
window.addEventListener("load", () => {
const main = document.querySelector("main");
for (const [id, { tx, tz, ry }] of articles.entries()) {
main.appendChild(makeArticleElement(id, tx, tz, ry));
}
document.addEventListener("click", ({ target }) => {
const targetId = target.closest("article")?.id;
let [itx, itz, iry] = [0, 0, 0]; // inversed transformation to apply to the main element
if (targetId && main.dataset.focus !== targetId) {
// zoom in
const { tx, tz, ry } = articles.get(targetId) || {};
[itx, itz, iry] = [tx, tz, ry].map(inverseTransformation);
main.setAttribute("data-focus", targetId);
} else {
// zoom out
main.removeAttribute("data-focus");
}
main.style.transform = `rotateY(${iry}) translate3d(${itx}, 0, ${itz})`;
});
});
// e.g. turn "90%" into "-90%" or "-10vmin" into "10vmin"
function inverseTransformation(transform) {
if (!transform) return 0;
const [_, value, unit] = transform.match(/(-?\d+)(.*)/);
return `${-Number(value)}${unit}`;
}
function makeArticleElement(id, tx = 0, tz = 0, ry = 0) {
const img = document.createElement("img");
img.src = `https://images.pexels.com/photos/${id}/pexels-photo-${id}.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500`;
const button = document.createElement("button");
button.appendChild(img);
const element = document.createElement("article");
element.id = id;
element.style.transform = `translate3d(${tx}, 0, ${tz}) rotateY(${ry})`;
element.appendChild(button);
return element;
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.