The vinyl shelf was about flipping through a crate of records, using pure 2D transforms with a spring config. When I started on the writing section, I didn’t want a second crate; I wanted something that felt like picking a card off a table.
The card stack reuses the shelf’s spring deflection and velocity gating, then adds depth. CSS perspective and translateZ give it a Z axis that 2D transforms cannot reproduce.
Resting tilt
Before you hover anything, every card leans back 3 degrees:
const REST_TILT = 3;
let targetRotateX = REST_TILT;
With perspective: 500px on the container, the three-degree tilt puts the top edge of each card farther away than the bottom. The stack looks as if it lies on a tilted surface. The vinyl shelf stays flat at rest; this angle gives the cards depth before interaction.
Picking up a card
When you hover, the card doesn’t just slide upward. It comes toward you along the Z-axis and tilts forward to face you:
if (isHovered) {
targetY = -18;
targetRotateX = -6;
targetTranslateZ = 40;
targetScale = 1.02;
}
The translateZ: 40 does most of the work. With a 500px perspective, 40px of Z travel makes the card appear about 9% larger through foreshortening, so it needs little explicit scaling. The rotateX: -6 moves it from a three-degree backward lean to a six-degree forward lean. Together, those transforms make the card lift and face the viewer.
The neighbours do the inverse. They tilt further back and sink into negative Z:
targetRotateX = REST_TILT + 4 * Math.exp(-Math.abs(distance) * 0.3);
targetTranslateZ = -20 * Math.exp(-Math.abs(distance) * 0.4);
At translateZ: -20, the immediate neighbour appears about 4% smaller through the same perspective maths. Exponential decay concentrates the push around the focused card: it advances while its neighbours retreat, like lifting a postcard from a stack.
The editorial card
The other half of the differentiation is visual. The vinyl shelf is imagery; album art does the talking. For writing, the card needs to be typographic. I leaned into a newspaper clipping metaphor: serif font for the title and snippet, a double-rule <hr> as a header divider, monospace tag in uppercase, tabular-nums on the date.
<hr className="border-hairline m-0 border-t-[3px] border-double" />
<h3 className="font-serif text-[0.9rem]/[1.2] font-bold text-balance">
{card.title}
</h3>
Each card is 180×250px, a portrait ratio like a broadsheet column and tall enough to show a title, date, tag, and a few lines of the snippet before it clips. The .text-balance on the title avoids orphaned words. The serif + monospace pairing makes each card feel like a small printed artefact rather than a UI element.
Dual springs
The vinyl shelf uses one spring for everything; the card stack uses two. SPRING_LIFT (stiffness 260, mass 0.9) gives the focused card a light response. SPRING_PUSH (stiffness 140, mass 1.2) gives the neighbours more weight. Their different responses reinforce which card has focus.
The finished stack
The finished stack combines resting tilt, Z-depth on hover, perspective foreshortening, newspaper card design, and the shelf mechanics underneath. prefers-reduced-motion gets a clean five-column grid. Mobile gets a horizontal scroll strip with snap points.
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.