Chapter 13
The anatomy of a card
A card looks like the most innocent component on the web: a rectangle with a border. But that rectangle has to say what order to read in, where you click, how much is underneath, and what's honest to count. Five cards, five different crafts hidden in the same shape.
The card is the component everyone thinks they know how to build. A rectangle, a border, a soft shadow, something inside. It looks like the most innocent brick on the web — and that’s exactly why it’s the one most often gotten wrong, because the shape is so familiar it hides how many decisions it holds. A card isn’t a container: it’s an object that declares a hierarchy. It says what order to read what it holds, where the clickable target ends, how much lies beneath the surface, and — when it reports a number — whether that number is telling the truth.
The question that opens the chapter is deliberately naïve: “is a card just a rectangle with a border?” No. Under the same profile hide different crafts. The editorial card orders a reading. The stat card reports a figure without inflating it. The action card wants to be fully clickable without breaking text selection. The expanding card has to grow where it stands, not tear the page open with a modal. The stack of cards promises there’s more underneath, and must keep that promise for someone navigating by keyboard.
Five cards, five problems the shape won’t let you see. And as always, no libraries: HTML already knows the link, the button, the list, the image with its proportion. Our job is to arrange them so the rectangle tells the truth about what it holds and how you touch it.
The card that orders a reading
The first is the most classic: image, eyebrow, title, lede. It looks like mere layout, but this is where hierarchy is decided — and where one technical detail, the image, can make everything else jump.
Six in the morning, and why nobody photographs that light well
An hour the manuals call “magic” and that almost always comes out flat. It isn’t the camera: it’s what we do in the ten minutes before.
Editorial card · hierarchy and derived state with :has()
The image isn’t a file: it’s a gradient placeholder with a fixed aspect-ratio. The difference isn’t aesthetic, it’s honesty toward the layout: the image’s shape exists before its content, so when the real photo arrives it won’t shove the title down mid-read — none of that sudden jump (the infamous layout shift) that makes you lose your place. Above it, the hierarchy is explicit: a monospaced small-caps eyebrow, a title that carries weight, a lede that explains. The eye knows at once where to start.
The hover state is derived, not glued on. Instead of tying the effect to “:hover on the title” or “:hover on the image” — which would light up in pieces — the card uses :has(.link:hover): “if you contain the link the pointer is on, light up as a whole.” Title, arrow and border react together, because the card is the target, not its disconnected parts. And with prefers-reduced-motion the transitions collapse: the lighting stays, the journey doesn’t.
The card that reports a number
A card can also just state a figure. When it does, the number is the protagonist — and how it enters the scene is a small test of honesty.
Stat card · honest count-up, data that doesn't inflate
The number climbs from zero to the real value when the card enters the viewport: once only, with out easing — fast at the start, calm at the arrival — in about 900ms. Never in-out, which reads as hesitant; never re-animate if the user scrolls away and back. The digits use tabular-nums: they all share the same width, so as they rise they don’t flinch and a column of numbers doesn’t dance. The count-up gives the figure a moment on stage, it doesn’t turn it into a show.
And the data tells the truth. 84% is 84%, not “99 to look good”; the review average drops by a tenth, and that decline is reported — with the ▼ sign before the colour — instead of vanishing. Colour is always the second signal, never the only one. Under prefers-reduced-motion the number appears already final: an explicit matchMedia guard in JS decides it, because a counting loop doesn’t stop on its own just by lowering a duration. Same value, no journey.
The card that is all one target
Third: the action card, the kind in a list of documents or files. We want the whole surface to be clickable — big targets are easier to hit — but without sacrificing text selection or the small actions that live inside it.
Open the document: March report
The title opens it. The icons stay separate actions.
Quick action card · stretched-link and reachable sub-actions
The temptation is to wrap the whole card in an <a>. It breaks two things: the text no longer selects, and every sub-action ends up nested inside a link — invalid and inaccessible. The honest technique is the stretched-link: a normal <a> on the title, with an ::after that stretches to cover the card. A click on the empty space lands on the link; the text stays selectable, because the overlay is a pseudo-element, not the text. A clickable card that doesn’t stop you from copying its name.
The sub-actions — share, more options — rise above the overlay with position: relative and z-index: they sit in front of the link that covers everything, so they stay clickable and tabbable. Each is a real <button> with its own aria-label, not a fake button. The rule: one big target for the main action, small independent targets for the secondary ones, and neither eating the other. No JS: it’s all browser state.
The card that grows where it stands
Fourth: the card that hides a detail and reveals it on request. The lazy answer would be a modal — but for an “I want to know more” a modal is too much: it tears away the context and asks to be closed. The card can simply grow.
Parcel in transit
Estimated arrival Thursday. Three of five stops completed.
Recorded stops
- Mon · Collected at the origin depot
- Tue · In transit to the regional hub
- Wed · Out for delivery to the city centre
Times are the carrier’s own, not our estimates.
Expandable card · grows in place, no modal
The trigger is a real <button aria-expanded>, not a hidden :checked nor a <div> with a listener. The state is real: a screen reader announces “expanded / collapsed”, the keyboard works for free, the panel is wired up via aria-controls. The card grows where it stands, pushing down what’s below: the eye never loses the thread, no dark surface drops over the page, nothing to close. The detail joins the flow, it doesn’t interrupt it.
The height transition is honest: grid-template-rows going from 0fr to 1fr. The grid measures the real content and animates exactly to it. That’s the difference from the old max-height “big enough” shortcut: that one guesses and, if the content is shorter, the animation snaps halfway and lands badly. Under prefers-reduced-motion the panel opens without growing: the duration token collapses, the final state is the same, only the movement disappears.
The card that promises depth
The last isn’t one card: it’s three, in a stack. A stack says “there’s more underneath” — and it has to say it to the eye with shadow and to the finger (or the tab key) by keeping every card genuinely reachable.
Hover or tab in: the stack fans out.
Card stack · OKLCH depth and an accessible fan
Depth comes from shadow, and here the shadow is built in OKLCH with color-mix on the accent: a warm, perceptually uniform tint, not a flat black that muddies. The cards behind are slightly smaller and lower — the perspective says “they’re back there” without hiding them. At rest it’s a compact stack; on hover or focus it fans out, the cards opening like a lectern just enough to say “there are three, distinct.” Not a deck exploding: a measured opening.
The delicate point of any stack is accessibility. Each card is a real <a>, in the flow, in reading order: the fan is only a transform, it doesn’t touch the tab order, so no card stays buried and unreachable. And when a card receives focus the stack opens on its own — :focus-within — because the focused card shouldn’t stay hidden under the others. With prefers-reduced-motion the fan disappears: the stack stays still and ordered, and focus shows in the border, not in the movement. Reachable either way.
Five cards, one discovery: the rectangle is the least of it. What a card does is declare — the reading order, the boundary of the target, the truth of a number, where the extra detail goes, how much lies under the surface. The editorial card orders, the stat card reports without inflating, the quick-action makes itself fully clickable without stealing your text, the expandable grows instead of tearing, the stack promises depth and keeps it even by keyboard. Border and shadow are the last thing you decide. First comes the question of what, really, this card is saying to whoever looks at it.
The next chapter leaves the objects you touch and looks at the space between them — emptiness as a design material. Is the silence in a page drawn, or just left to happen? How white space stops being absence and becomes rhythm, hierarchy, breath.