Split-flap board
CSS + JSA departure board: each character is four half-height leaves, and a change drops the old top half and lands the new bottom half. JS only swaps the letters and restarts the CSS animation.
A red and a cyan copy of one word, blended with screen. The pointer sets how far apart they sit and turns the card, like old red/cyan cinema glasses.
Your edited version
translate() and blended with mix-blend-mode: screen — like old red/cyan 3D glasses, where the overlap reads near-white and the fringes stay coloured.rotateY / rotateX); it is deliberately not preserve-3d, because a blend mode only combines elements painted into the same flat surface.--sx, --sy, --ry, --rx); every animated value in the CSS just reads one of them..is-live); once it leaves, the slower springy transition takes over for the way back to rest.mix-blend-mode: screen on the leavespointer → --sx / --ry / --rxflat card, no preserve-3d around a blendfast transition live, springy on release<div class="scene">
<div class="anaglyph">
<div class="word">
<span>STEREO</span>
<span aria-hidden="true">STEREO</span>
</div>
<small>move your pointer</small>
</div>
</div>
.scene {
perspective: 800px;
}
.anaglyph {
display: grid;
place-items: center;
gap: 2px;
width: 214px;
padding: 24px 0 16px;
border: 1px solid #3a4070;
border-radius: 16px;
background: radial-gradient(circle at 50% 30%, #171b38, #07080f 75%);
box-shadow: 0 22px 34px -22px rgb(0 0 0 / 0.85);
/* the card turns as ONE flat plane in the stage's perspective; not preserve-3d, because the
screen blend below only works among elements painted into the same flat surface */
transform: rotateY(var(--ry, -14deg)) rotateX(var(--rx, 6deg));
transition: transform 0.7s cubic-bezier(0.3, 1.3, 0.5, 1);
}
.word {
position: relative;
font-size: 43px;
font-weight: 900;
line-height: 1;
letter-spacing: 0.02em;
user-select: none;
}
/* two copies of the word, one per eye, pushed apart horizontally; screen adds their light,
so where red and cyan overlap you get near-white and the fringes stay coloured */
.word span {
display: block;
color: #ff1744;
mix-blend-mode: screen;
transform: translate(calc(var(--sx, 3px) * -1), calc(var(--sy, 0px) * -1));
transition: transform 0.7s cubic-bezier(0.3, 1.3, 0.5, 1);
}
.word span + span {
position: absolute;
inset: 0;
color: #00e5ff;
transform: translate(var(--sx, 3px), var(--sy, 0px));
}
.anaglyph small {
color: #949bc0;
font-size: 9.5px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
}
/* while the pointer is over the stage, follow it almost directly; the slow springy transition
above is only used on the way back */
.anaglyph.is-live,
.anaglyph.is-live span {
transition-duration: 0.12s;
transition-timing-function: ease-out;
}
var stage = document.querySelector('.scene');
var card = document.querySelector('.anaglyph');
function clamp(v, min, max) {
return Math.min(max, Math.max(min, v));
}
function move(e) {
var r = stage.getBoundingClientRect();
var x = clamp((e.clientX - r.left) / r.width - 0.5, -0.5, 0.5);
var y = clamp((e.clientY - r.top) / r.height - 0.5, -0.5, 0.5);
card.style.setProperty('--sx', (x * 16).toFixed(2) + 'px');
card.style.setProperty('--sy', (y * 5).toFixed(2) + 'px');
card.style.setProperty('--ry', (x * 44).toFixed(1) + 'deg');
card.style.setProperty('--rx', (-y * 30).toFixed(1) + 'deg');
card.classList.add('is-live');
}
function leave() {
card.classList.remove('is-live');
['--sx', '--sy', '--ry', '--rx'].forEach(function (p) {
card.style.removeProperty(p);
});
}
stage.addEventListener('pointermove', move);
stage.addEventListener('pointerdown', move);
stage.addEventListener('pointerleave', leave);
stage.addEventListener('pointercancel', leave);
// Red/cyan stereo text. JS writes three numbers on the card: --sx (eye separation), --ry, --rx.
// The card is always dark, because `screen` blending needs a dark backdrop to add light to.
.d-anaglyph {
display: grid;
place-items: center;
gap: 2px;
width: 214px;
padding: 24px 0 16px;
border: 1px solid var(--border-strong);
border-radius: 16px;
background: radial-gradient(circle at 50% 30%, #171b38, #07080f 75%);
box-shadow: 0 22px 34px -22px rgb(0 0 0 / 0.85);
// The card turns as ONE flat plane in the stage's perspective. It is deliberately not
// preserve-3d: blending only works among elements painted into the same flat surface.
transform: rotateY(var(--ry, -14deg)) rotateX(var(--rx, 6deg));
transition: transform 0.7s cubic-bezier(0.3, 1.3, 0.5, 1);
&__word {
position: relative;
font-size: 43px;
font-weight: 900;
line-height: 1;
letter-spacing: 0.02em;
user-select: none;
// Two copies of the word, one per eye, pushed apart horizontally. `screen` adds their
// light: where red and cyan overlap you get near-white, the fringes stay coloured.
span {
display: block;
color: #ff1744;
mix-blend-mode: screen; // on the leaves only, never on a 3D container
transform: translate(calc(var(--sx, 3px) * -1), calc(var(--sy, 0px) * -1));
transition: transform 0.7s cubic-bezier(0.3, 1.3, 0.5, 1);
}
span + span {
position: absolute;
inset: 0;
color: #00e5ff;
transform: translate(var(--sx, 3px), var(--sy, 0px));
}
}
small {
color: #949bc0;
font-size: 9.5px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
}
// While the pointer is over the stage follow it almost directly; the slow springy
// transition above is only for the way back.
&.is-live,
&.is-live span {
transition-duration: 0.12s;
transition-timing-function: ease-out;
}
}