Card

A surface that holds content. It paints; it does not arrange.

import { Card } from "@dede-style/ui/card", then <Card elevation="raised">…</Card>. Width comes from the lane it sits in and the rhythm of its children comes from the Sleeve underneath it, so the only things this component decides are the fill, the edge, the shadow and the inset.

Reserved seating

A card is a fill, an edge and an inset around something. The something is ordinary content, spaced by the Sleeve underneath — which is why two paragraphs sit correctly here without the card knowing they are paragraphs.

Change the inset and watch this text move; change the rung and watch the ground move under it.

Everything it has not got is the same sentence twice. There is no width or maxWidth, because the lane already sets the measure and two things that can cap a box means you cannot tell from reading the markup which one did. There is no direction, because arranging is the Sleeve’s job. There is no colour axis, because this system reserves colour for state.

What is left is a ladder of three grounds, an edge you can turn off, and an inset that reaches zero.

Elevation

A fill ramp with a direction, not a shadow ramp — which is what makes it read the same either side of the theme toggle.

  • sunken

    Set into the page. A well, a track, a region you are meant to read as recessed.

  • flush

    The page's own plane. The edge is the only thing making it a card, which is why the border is on by default.

  • raised

    Lifted off the page — a lighter fill in both modes, with a shadow reinforcing it.

Raised is lighter than the page and sunken is darker, in both modes. That is the whole design: the surface roles are ordered by elevation rather than by distance from black, so a card cannot end up darker than the page it sits on when the theme flips.

Three rungs, and the number is arithmetic. On the light mode raised is pure white, the top of the neutral ramp with nothing above it. On the dark theme sunken is eleven byte values off black, with almost nothing below it. Each mode is closed at the opposite end, so three is the longest ladder that behaves identically in both — which is the property worth having.

The shadow reinforces the fill; it is not the signal. On the dark mode a black shadow against a near-black page is very nearly nothing, and this component is the first real consumer of the shadow scale — so it is the evidence for that gap rather than the place it gets fixed.

The edge

On at every rung by default, and doing a different job at each one.

  • Raised, edged

    The default. Two edge treatments at once — a line and a shadow — which most systems drop one of.

  • Raised, no edge

    The shadow carries it alone. The box is the same size: the border stays and loses its colour, so nothing moves.

  • Edged

  • No edge

  • Edged

  • No edge

The second row is the one that decides it. A fill difference that reads at panel size does not necessarily read at chip size — the same four tone units spread across a card is a step you can see, and across a chip it is a suggestion. A shadow shrinks with the box it is cast by; a hairline does not.

At flush the fill is the page’s own colour, so the edge is the only thing making it a card at all — which is why the default is on rather than per-rung. A borderless flush card is deliberately invisible: that is what “the page’s own plane” means, and quietly substituting a rung nobody asked for would be worse than showing nothing.

The inset, and why it reaches zero

A step on the fixed component scale, because this holds content off an edge rather than a region off a region.

  • A media card

    The image is a child, bled to the card’s own corners because the card clips itself.

  • The default inset

    24px — the band’s inside rhythm. A card sits inside a band, so it takes the inside number rather than the 48 a Section spends against the viewport edge.

A rounded card that cannot clip its own content is not rounded. Without overflow: clip the block on the left would keep its square corners inside the rounded box — visible, wrong, and expensive to retrofit once callers have worked around it. It costs one declaration now.

The inset is fixed, and it does not breathe with the container. That is the same split that produced Sleeve: the distance between a heading and the thing it names is a property of the pair, not of the window, and so is the distance between text and the edge holding it. Regions space on the fluid scale; content spaces on the fixed one.

Its size belongs to whatever holds it

A card fills the width of its container and takes the height of its content. Drop one into a page and it needs nothing; when you want something else, you tell the parent.

Equal heights in a row is <Grid align="stretch">, which is the default — so the cards above match without asking. Each card hugging its own content is <Grid align="start">. Horizontally the same job belongs to the region: <Slot justify="start"> shrinks a card to its content instead of filling the lane.

Only the container knows what the alternatives are. A card cannot see its neighbour, so it cannot know whether it should match one. The row can. Put the dial on the card and you would set it three times and they could disagree; on the Grid it is one declaration that cannot be inconsistent. It is the same reasoning that keeps alignSelf off Sleeve — the capability is not lost, it lives on the component that owns the space being divided.

Hugging a card full of prose looks like it does nothing, and that is arithmetic rather than a bug. Shrink-to-fit resolves to min(max-content, available), and a paragraph’s max-content — every line unwrapped — is far wider than any lane. So it hugs only when the content is genuinely narrow: a label, a control, an image of a fixed size.

A card that is not in the flow is a different question. A dialog is positioned, so there is no lane to take a width from and nothing would size it — which makes width that component’s own property to own. In flow, the lane decides; out of flow, the component does.

It is a Sleeve underneath

<Card>…</Card> renders exactly one element: <div data-component="Card" class="dede-sleeve dede-card">. Not a card wrapping a sleeve — the sleeve is the card, wearing a second class. Composing rather than wrapping costs no extra node, which is the obvious argument, and it is not the important one.

The important one is that a hand-rolled panel spaces its paragraphs twice. Two paragraphs are separated by an element margin in the base stylesheet, and the only thing that switches that margin off is being inside a Sleeve. A fresh display: grid; gap box therefore applies both the margin and the gap, and every pair of paragraphs in it is spaced by the sum. That bug has already shipped once here, in the panel this component replaces.

So gap and justify are forwarded rather than reimplemented, and nothing outside this component ever sees the Sleeve. A card is a surface with a content container already inside it, which is what makes it the shortest thing in the chain that can hold a heading and a paragraph and get the spacing right.