Click a tape to open it
Night Bus Home
Lowercase Lions
00:00
01 / 12
How I Built My Tape Deck
Six divs, one tilted shelf, a few springs and a tiny drum machine: a step-by-step, beginner-friendly look at my 3D cassette shelf.
TL;DR
TapeDeck is a React component that shows a 3D shelf of cassette tapes. Each tape is a box built from six divs with CSS 3D transforms, all tapes sit on one tilted parent, and spring physics in a single requestAnimationFrame loop moves them without React re-renders. Tapes without an audio file play a beat generated with the Web Audio API.
On my homepage there's a shelf of cassette tapes showing the songs I've played recently. Hover a tape and it pops up. Click it and it flies out to the front, the little reels start spinning and music plays. In the code it's a React component called TapeShelf. It looks complicated, but it's really a handful of simple ideas stacked on top of each other. Here they are, in the order I built them.
The plan in four steps
- Build one tape. A cassette is just a thin box, and a box is six flat divs folded into shape with CSS.
- Line the tapes up on a tilted shelf. Tilt one parent element instead of every tape.
- Make them move with springs. That's what makes it feel bouncy and alive instead of robotic.
- Add sound. Play a song if the tape has one, or make a beat right in the browser if it doesn't.
Step 1: a box made of six divs
Think of a cardboard box you fold from a flat sheet. In CSS, each side is a div. You rotate each div so it faces the right way, then slide it out from the centre by half the box's size. The front slides forward, the top tips over and slides up, and so on.
One line makes it all work: transform-style: preserve-3d on the parent. Without it the browser squashes the children flat, like a photo of a box instead of a real one.
.tape { transform-style: preserve-3d; } /* keep my children in 3D */
/* --w, --h, --d = width, height and depth of the tape */
.front { transform: translateZ(calc(var(--d) / 2)); } /* slide forward */
.back { transform: rotateY(180deg) translateZ(calc(var(--d) / 2)); } /* turn around, slide back */
.top { transform: rotateX(90deg) translateZ(calc(var(--h) / 2)); } /* tip over, slide up */
.left { transform: rotateY(-90deg) translateZ(calc(var(--w) / 2)); } /* turn sideways, slide left */The front face is where the details live: the paper label (any JSX you like, such as an album cover), a little window, and two reels. Each reel is just a circle filled with a repeating-conic-gradient, which draws the spokes. When the tape plays, a CSS animation spins them.
Step 2: one tilted shelf
My first idea was to tilt every tape on its own. That got messy fast. The easier way is to put all the tapes inside one parent, the “rail”, and tilt only the rail. Now each tape just needs to sit a bit further back than the one before it.
// The whole shelf leans by this much.
const RAIL = { rotateX: -20, rotateY: -40, left: 0.26, top: 0.84 };
// Tape number i sits further back along the rail.
const z = -(i - offset.value) * gap;Here's the neat part about opening a tape. The open tape rotates by the exact opposite of the rail's tilt, so the two cancel out and the tape ends up facing you straight on. Then it slides to the middle of the screen and comes a little closer.
// f goes from 0 (sitting on the shelf) to 1 (fully open)
el.style.transform =
`translate3d(0, ${-lift}px, ${z}px) ` +
`rotateY(${-RAIL.rotateY * f}deg) rotateX(${-RAIL.rotateX * f}deg) ` + // undo the tilt
`translate3d(${toX * f}px, ${toY * f}px, ${FOCUS_Z * f}px) ` + // fly to the front
`scale(${1 + (scale - 1) * f})`;Because everything is multiplied by f, a single number from 0 to 1 controls the whole trip. At 0 the tape is on the shelf, at 1 it's open, and halfway it's halfway. The tape also gets a small lift so it arcs up and out of the row instead of sliding through its neighbours.
Step 3: springs instead of timers
A normal CSS transition always takes a fixed time, say 300ms. That feels a bit stiff, and if you click again halfway through, it jumps. A spring works differently. It just keeps pulling a value toward a target, like a rubber band. If you change the target midway, it turns around smoothly from wherever it is.
The whole spring is three lines of maths, run on every frame:
const force = s.stiffness * (s.target - s.value) - s.damping * s.velocity;
s.velocity += force * dt; // the pull speeds it up…
s.value += s.velocity * dt; // …and the speed moves it- Stiffness is how hard the rubber band pulls. Higher means faster.
- Damping is friction. Lower means more bounce.
stiffness 130 · damping 22
Every tape has two springs: focus (that's the f from step 2) and lift for the hover pop. The shelf has a few more: one for scrolling, one that sinks the shelf while a tape is open, and two that tilt the open tape a little as your mouse moves. To open a tape I just set its focus target to 1. The spring takes care of the rest.
Keeping it smooth
If React re-rendered the component 60 times a second, the shelf would stutter. So the job is split in two:
- React state only remembers things that change rarely, like “which tape is open”.
- One animation loop (
requestAnimationFrame) steps every spring and writes thetransformstraight onto each tape's DOM element through a ref.
The loop also puts itself to sleep. When every spring has settled, it stops asking for frames, so a shelf that isn't moving uses no CPU at all. Any click, hover or drag wakes it up again with a small kick() function.
const tick = (now) => {
const dt = Math.min(0.032, (now - last) / 1000);
last = now;
// renderFrame returns true while anything is still moving
frame.current = renderFrame(dt) ? requestAnimationFrame(tick) : 0;
};Step 4: music, even without music files
A tape can have a src, an audio file to play. On my homepage that's the Spotify preview of the song. If a tape has no audio, a tiny drum machine takes over. The Web Audio API can make sound from scratch:
- Kick drum: a tone that quickly drops from 140Hz to 40Hz. That's the “boom”.
- Snare and hi-hats: short bursts of random noise, filtered so they sound crisp.
- A few soft notes on top, so it isn't only drums.
The tape's bpm sets the speed (and how fast the reels spin), and its id nudges the pattern so every tape sounds a little different. Notes are scheduled slightly ahead of time, so the beat stays steady even if the page is busy.
The little things
- Drag or scroll sideways to browse. Vertical scrolling is left alone so the page still scrolls normally.
- Keyboard works too: arrow keys to move, Enter to open, Space to play, Escape to close.
- Other tapes dim while one is open, so your eye goes to the right place.
- Reduced motion is respected. If someone has it turned on, the springs jump straight to their target instead of animating.
Using it
Give it a list of tapes. A cover can be the built-in TapeCover helper, an image or any JSX:
import { TapeShelf, TapeCover } from "@/components/tape-shelf";
<TapeShelf
tapes={[
{
id: "night-bus",
title: "Night Bus",
artist: "Lowercase Lions",
cover: <TapeCover background="#2fc0c9" text="NIGHT BUS" />,
bpm: 92, // no src, so it plays a generated beat
},
]}
/>That's the whole trick: six divs make a box, one tilted parent makes a shelf, a few springs make it feel alive, and a small drum machine fills the silence. None of these ideas is hard on its own. They just add up to something that feels like a real object you can pick up.
FAQ
- How do you make a 3D box with CSS?
- Use six divs for the faces inside a parent with transform-style: preserve-3d. Rotate each face to its side (for example rotateX(90deg) for the top) and then translateZ it out from the centre by half the box's size.
- Why use springs instead of CSS transitions?
- A spring keeps pulling a value toward its target, so changing the target mid-animation turns it around smoothly instead of jumping. Stiffness controls speed and damping controls bounce.
- How does it stay smooth?
- React state only tracks which tape is open. One requestAnimationFrame loop steps every spring and writes transforms directly to the DOM, and the loop stops when all springs are at rest.
- Where does the music come from?
- A tape can have an audio file. Without one, a small drum machine built on the Web Audio API plays a kick, snare, hi-hats and a few notes at the tape's bpm.