A coin that isn’t there
How to create a 3D coin with CSS.
div.The main challenge
There are no cylindrical shapes in CSS 3D, only flat planes. So, to work around this, we need a trick, a visual illusion.
Here's one: you don't need to think about believable volumetric shape distortion when there's no vanishing point. Without perspective, rotation is a squash.
With no
perspectiveset, a turning element does not change shape and only gets narrower. Turn it by θ and its width on screen iscos θof what it was: full width flat on, 71% at forty-five degrees, nothing at ninety.A surface sitting at some depth behind the front does not stay put while the coin turns — it slides sideways, by its depth times
sin θ. So every surface in the coin comes down to two numbers: how narrow it got, and how far it slid.
Five surfaces
The markup gives each coin three children — .coin-face, .coin-edge, .coin-back — but there are five real surfaces, because the edge carries a ::before and an ::after. The edge photograph is used three times per coin: once as a narrow slab stood side-on, and twice as a full-size disc sitting in the front and back face planes.
Pull the five apart and look at what you took for the side of the coin. It isn’t the slab: that narrow strip spends most of the turn hidden behind the obverse. It’s the back disc, showing from behind the face.
Push them back together and switch the back disc off on its own to confirm it: the volume leaves with it.
What makes the substitution hold is that the five surfaces add up to the right outline. Their combined silhouette is a rectangle flanked by two half-ellipses — which is precisely the parallel projection of a cylinder. The coin is not a cylinder; it just casts the same shadow as one.
translateZ cannot change what you see except through rotation.The edge that isn’t there
CSS can draw the two faces — a circle is a div with border-radius — but not the cylinder between them. So the job is to show the reeding photograph in the gap between the two planes, clipped to whatever shape that gap has at the current angle.
The code never computes that clip. It puts the reeding on a disc the size of the face, in the opposite plane, and the face hides all of it except the crescent that has slid out from behind. Past 90° the reverse does the same for the other disc.
The outline is right, but the picture inside it is fake, and the reeding hides that. Along the ridges, the axis the code stretches, there are no landmarks at all. The strip is pulled to an arbitrary one and a half coin diameters. The ridges lie on a flat disc and compress linearly, where on a real rim they would crowd toward the edge of the silhouette. And the slab and the discs share one background-size at very different widths, so the slab shows about 6% of the picture and the discs about 67%.
Rule the lines across that axis and all three show up. The geometry doesn’t change.
And it all relies on the other half of the opening: no perspective. Without it both discs squash by the same cos θ, so the far one is just the near one slid sideways. With it the far disc shrinks, the slab tapers, and the coin picks up a camera position. In a row each coin would sit at a different angle to the vanishing point, and comparing denominations, the whole point of the original page, stops working.
Light that isn’t there
Nothing on this page is lit. There is no light source, no material, no surface normal to take a dot product against — and the coins still read as metal sitting in a room. Two flat films and one shadow do all of it, and each is driven by the same --rotate that turns the coin.
The obverse darkens as it swings away: a black film at rotate × 0.002, about a third of an opacity point by the time it is edge on. The reverse carries the opposite, a veil starting at 0.63 that thins as that face comes round toward you.
The third, the reflex, is the one worth stealing. The obverse casts a box-shadow coloured with var(--bg), the colour of the ground the coin stands on, so against that ground it is invisible. It only shows where it falls on the neighbouring coin, washing the ground’s colour across it — which is what a real coin on a real table does. Whether it reads as light or as a stain depends on which side it arrives from, and nothing in the code decides that. The veil darkens the face turning away, the reflex brightens the edge turning toward you, and the two agree only because the coins overlap the way they do. Reverse the row and the reflex comes from the shadow side, lighting the face the veil is darkening.
None of this needs 3D
Every surface is a plane at a fixed depth, so every one of them is nothing but scaleX(cos θ) plus a sideways offset of d · sin θ; the slab, being perpendicular, is scaleX(sin θ). Five 2D transforms, the same pixels, no preserve-3d, no depth at all. CSS has cos() and sin() now, so it does not even need script.
What the 3D actually buys is bookkeeping. It does the trigonometry, so one rotateY per coin replaces five hand-derived expressions that would have to be kept mutually consistent. It culls the backfaces, so obverse and reverse swap over at 90° by themselves. And it sorts the layers by depth, so the crescent emerges from behind the face without anyone maintaining a z-order that changes halfway through the turn.
The illusion is built out of flat pictures and a handful of scalar expressions. The 3D is there to keep those expressions honest, not to make the volume.