A fake rolodex for site navigation. Cards and tabs are real, persistent DOM elements, flipping just changes their position/z-index in an array, so CSS transitions animate them rolling into place instead of popping between states.
<!-- markup --> <div class="rolodex"> <div class="roll-stage" id="rollStage"> <div class="tab-layer" id="tabLayer"></div> </div> <div class="roll-controls"> <button class="roll-btn" id="rollPrev">‹ back</button> <div class="roll-dots" id="rollDots"></div> <button class="roll-btn" id="rollNext">flip →</button> </div> </div> /* css */ .rolodex { max-width: 420px; margin: 60px auto 0; } .roll-stage { position: relative; height: 340px; } .tab-layer { position: absolute; top: 0; left: 0; right: 0; height: 0; overflow: visible; pointer-events: none; } .fan-tab { position: absolute; top: 0; left: 8px; width: 38px; height: 38px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.05rem; background: var(--tab-color); box-shadow: 0 3px 8px rgba(0,0,0,.15); cursor: pointer; pointer-events: auto; transition: transform .55s cubic-bezier(.45,.05,.35,1), opacity .4s ease; } .fan-tab.active { box-shadow: 0 6px 16px rgba(0,0,0,.25); } .roll-card { position: absolute; inset: 0; background: #fff; border: 3px solid var(--card-color); border-radius: 22px; padding: 1.75rem; transition: transform .55s cubic-bezier(.45,.05,.35,1), opacity .35s ease; box-shadow: 0 10px 26px rgba(0,0,0,.12); display: flex; flex-direction: column; } // javascript const pages = [ { icon: '🌸', title: 'Blog', color: '#FF9EBB', href: 'blog.html' }, { icon: '🎮', title: 'Games', color: '#8FCBE0', href: 'games.html' }, { icon: '💻', title: 'Code', color: '#8FBF9F', href: 'programming.html' }, { icon: '🎬', title: 'Cinema', color: '#F0C27B', href: 'movies.html' }, { icon: '🖥️', title: 'System', color: '#C9A6E0', href: 'terminal.html' } ]; let order = pages.map((_, i) => i); // current stack order, front = order[0] // build ONE persistent element per page — never destroyed/recreated, // which is what lets CSS transitions animate a flip instead of snapping const cardEls = pages.map(p => makeCard(p)); const tabEls = pages.map(p => makeTab(p)); function layout() { order.forEach((pageIdx, pos) => { const card = cardEls[pageIdx]; card.style.zIndex = (order.length - pos) * 10; card.style.transform = `translate(${pos*9}px, ${-pos*9}px) scale(${1-pos*.035})`; card.style.opacity = Math.max(1 - pos*.15, .55); const tab = tabEls[pageIdx]; tab.style.zIndex = (order.length - pos) * 10 + 5; // +5: sits just above its own card, // but still below any card further forward tab.style.transform = `translate(${pos*9+14}px, ${-pos*9-10}px) scale(${1-pos*.07})`; tab.classList.toggle('active', pos === 0); }); } function flip(direction) { const front = cardEls[order[0]]; front.style.transform = 'translateY(-6px) rotateX(-100deg)'; // flip the front card away front.style.opacity = '0'; setTimeout(() => { direction === 'next' ? order.push(order.shift()) : order.unshift(order.pop()); layout(); // everything else animates to its new spot automatically }, 420); }