Hover card fan
Pure CSSThree overlapping cards: the one you point at straightens and comes toward you while the others dim and lean away. Static strips catch the pointer, so nothing flickers.
Four slides on the sides of a box. JS counts steps; CSS turns the box by 90° per step.
Your edited version
rotateY(n × 90deg) translateZ(width / 2).translateZ(-width / 2), so whichever face is in front sits at z = 0 and renders at its true size.rotateY(calc(var(--step) * -90deg)).rotateY(calc(var(--step) * -90deg))unbounded counter = endless rotationtranslateZ(-w/2) keeps the front at z = 0<div class="slider">
<div class="scene">
<div class="box">
<i style="--hue:250">Design</i>
<i style="--hue:290">Build</i>
<i style="--hue:330">Ship</i>
<i style="--hue:10">Repeat</i>
</div>
</div>
<nav>
<button type="button" data-dir="-1" aria-label="Previous">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
</button>
<button type="button" data-dir="1" aria-label="Next">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
</button>
</nav>
</div>
.slider {
display: grid;
justify-items: center;
gap: 50px;
}
.scene {
perspective: 800px;
}
.box {
--w: 280px;
position: relative;
width: var(--w);
height: 170px;
transform-style: preserve-3d;
transform: translateZ(calc(var(--w) / -2)) rotateY(calc(var(--step, 0) * -90deg));
transition: transform 0.7s cubic-bezier(0.3, 1.25, 0.5, 1);
}
.box i {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: 8px;
color: #fff;
font: 900 2rem system-ui;
background: linear-gradient(135deg, hsl(var(--hue) 85% 62%), hsl(var(--hue) 75% 38%));
backface-visibility: hidden;
transform-style: preserve-3d;
}
/* plugs the see-through notches where the rounded corners of two slides meet */
.box i::before {
content: '';
position: absolute;
inset: 6px;
background: hsl(var(--hue) 75% 34%);
transform: translateZ(-6px);
}
.box i:nth-child(1) { transform: rotateY(0deg) translateZ(calc(var(--w) / 2)); }
.box i:nth-child(2) { transform: rotateY(90deg) translateZ(calc(var(--w) / 2)); }
.box i:nth-child(3) { transform: rotateY(180deg) translateZ(calc(var(--w) / 2)); }
.box i:nth-child(4) { transform: rotateY(270deg) translateZ(calc(var(--w) / 2)); }
nav { display: flex; gap: 8px; }
nav button {
width: 44px;
height: 36px;
border: 1px solid #5a6188;
border-radius: 8px;
display: grid;
place-items: center;
padding: 0;
background: #161a2e;
color: #fff;
cursor: pointer;
}
/* SVG arrows, not text like ‹ ›: a glyph sits on the font's baseline, so it never centres */
nav svg {
width: 18px;
height: 18px;
}
const box = document.querySelector('.box');
let step = 0; // never wraps, so the box keeps turning the same way
document.querySelector('nav').addEventListener('click', (e) => {
const btn = e.target.closest('[data-dir]');
if (!btn) return;
step += Number(btn.dataset.dir);
box.style.setProperty('--step', step);
});
@use '../mixins' as *;
$w: 190px;
.d-boxslider {
display: grid;
grid-template-rows: 1fr auto;
justify-items: center;
width: 100%;
height: 100%;
padding: 10px 14px 14px; // the control dock: the same place in every demo with controls
&__view {
display: grid;
place-items: center;
perspective: 700px;
}
&__box {
position: relative;
width: $w;
height: 120px;
transform-style: preserve-3d;
// --step is an ever-growing (or shrinking) integer written by JS.
transform: translateZ($w * -0.5) rotateY(calc(var(--step, 0) * -90deg));
transition: transform 0.7s cubic-bezier(0.3, 1.25, 0.5, 1);
i {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: 6px;
background: linear-gradient(135deg, hsl(var(--hue) 85% 62%), hsl(var(--hue) 75% 38%));
color: #fff;
font-size: 26px;
font-style: normal;
font-weight: 900;
backface-visibility: hidden;
@include solid-core(5px, hsl(var(--hue) 75% 34%));
@for $i from 1 through 4 {
&:nth-child(#{$i}) {
transform: rotateY(($i - 1) * 90deg) translateZ($w * 0.5);
}
}
}
}
&__nav {
display: flex;
gap: 8px;
button {
width: 34px;
height: 28px;
border: 1px solid var(--border-strong);
border-radius: 8px;
background: var(--surface);
display: grid;
place-items: center;
padding: 0;
cursor: pointer;
&:hover {
border-color: var(--warm);
}
}
}
}