Rotating cube
Pure CSSSix faces placed with rotate + translateZ, spun by a single keyframe animation.
Scroll inside the box. JS turns scroll progress into one number; CSS maps it to rotation.
Your edited version
--p, from 0 to 1.rotateY(calc(var(--p) * 720deg)). Change the feel by editing CSS only.position: sticky keeps the cube in view while the tall page scrolls past.animation-timeline: scroll() — check browser support before relying on it; the JS version works everywhere.scroll progress 0…1 in --pposition: sticky stageCSS-only alternative: animation-timeline: scroll()<div class="sticky">
<div class="cube">
<div></div><div></div><div></div>
<div></div><div></div><div></div>
</div>
</div>
<div class="spacer"></div>
body {
display: block; /* let the page scroll normally */
overflow: auto;
}
.sticky {
position: sticky;
top: 0;
display: grid;
place-items: center;
height: 100vh;
perspective: 700px;
}
.spacer {
height: 300vh;
}
.cube {
--s: 130px;
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
/* --p is scroll progress 0…1, set on <html> from JS */
transform:
rotateX(calc(-20deg + var(--p, 0) * 360deg))
rotateY(calc(var(--p, 0) * 720deg));
}
.cube > * {
position: absolute;
inset: 0;
background: rgb(255 181 71 / 0.32);
border: 1px solid rgb(255 181 71 / 0.85);
}
/* turn each face outward, then push it half a side from the centre */
.cube > :nth-child(1) { transform: rotateY(0deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(2) { transform: rotateY(90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(3) { transform: rotateY(180deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(4) { transform: rotateY(-90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(5) { transform: rotateX(90deg) translateZ(calc(var(--s) / 2)); }
.cube > :nth-child(6) { transform: rotateX(-90deg) translateZ(calc(var(--s) / 2)); }
const root = document.documentElement;
function onScroll() {
const max = root.scrollHeight - innerHeight;
root.style.setProperty('--p', max > 0 ? scrollY / max : 0);
}
addEventListener('scroll', onScroll, { passive: true });
onScroll();
@use '../mixins' as *;
.d-scrollspin {
--p: 0; // scroll progress 0…1, written by JS
position: absolute;
inset: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
// stays in view while the spacer scrolls past
&__sticky {
position: sticky;
top: 0;
display: grid;
place-items: center;
height: 100%;
perspective: 600px;
small {
position: absolute;
bottom: 8px;
color: var(--muted);
font-size: 12px;
opacity: calc(1 - var(--p) * 4);
}
}
&__spacer {
height: 300%;
}
&__cube {
--s: 90px;
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
transform: rotateX(calc(-20deg + var(--p) * 360deg)) rotateY(calc(var(--p) * 720deg));
@include cube-faces(var(--s));
i {
position: absolute;
inset: 0;
@include face(var(--warm), 32%);
}
}
}