Skip to content

product card

Card that represents a product, app, or other top-level entity in a grid. Used on the design.rechaj.com homepage; reusable for any “browse a list of things” surface.

┌────────────────────────────┐
│ [thumb area] │ ← fixed aspect ratio (4/3 mobile, 16/10 desktop)
├────────────────────────────┤
│ Title │ ← 2-line reserved slot
│ Title (continued) │
│ │
│ Description, kept to 2 │ ← 2-line reserved slot
│ lines max. │
│ │
│ [ Primary ] [ Secondary ] │ ← pinned to bottom
└────────────────────────────┘

Without reserved line counts, a single-line title or single-line description shrinks the body and pushes the action row up — so action buttons drift to different vertical positions across the row. The fix is to reserve two lines for title and two lines for description so every card has the same internal rhythm regardless of copy length.

The body uses CSS Grid with three rows (title / description / 1fr actions cell). The action row’s align-self: end pins it to the bottom of the 1fr cell — more deterministic than flex column + margin-top: auto, especially when the card itself sits inside a parent that’s also flex-derived.

.pcard {
display: flex;
flex-direction: column;
height: 100%; /* fill grid row */
border-radius: 16px;
background: #FFFFFF;
box-shadow: 2.083px 2.777px 15.967px 0.69px rgba(193,197,210,0.0266);
overflow: hidden;
}
.pcard-thumb { height: 220px; flex-shrink: 0; } /* fixed px height — see note below */
.pcard-body {
padding: 18px 20px 20px;
display: grid;
grid-template-rows: auto auto 1fr; /* title, desc, actions cell */
gap: 6px;
flex: 1; /* fill remaining card height */
min-height: 0; /* flex-in-grid sanity */
}
.pcard-title {
font: 600 18px/1.25 'General Sans';
letter-spacing: -0.3px;
min-height: calc(18px * 1.25 * 2); /* reserve 2 lines */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.pcard-desc {
font: 400 14px/1.5 'General Sans';
color: var(--text-secondary);
min-height: calc(14px * 1.5 * 2); /* reserve 2 lines */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.pcard-actions {
align-self: end; /* bottom of 1fr cell */
display: flex;
gap: 8px;
padding-top: 12px;
}

aspect-ratio computes the panel’s height from its rendered width. In a grid with 1fr columns, column widths can differ by sub-pixel amounts due to container-math rounding, and that propagates into thumb-height differences large enough to misalign the action rows below. A fixed height (e.g. 220px) sidesteps the math and guarantees pixel-identical panels — which is what “same height” requires.

If you need responsive thumb heights, define a few breakpoints with explicit height values (e.g. 200px mobile / 220px tablet / 240px desktop) rather than reaching for aspect-ratio.

  • ❌ Removing height: 100% from .pcard — body’s flex: 1 becomes meaningless when the card itself doesn’t fill the grid row.
  • ❌ Letting title or description size to content with no min-height — short copy causes the action row to jump up.
  • ❌ Putting the action row inside a wrapper that has its own padding-top or margin — that interferes with margin-top: auto.
  • ❌ Using text-align: center for the title in a left-aligned card — breaks rhythm with the description.

The reference implementation uses 18px / 14px for title / description. If you need a denser variant (e.g. dashboard tile lists), keep the rhythm rule and scale both sizes proportionally — never change just one or the line counts.

The canonical implementation is site/src/components/ProductCard.astro on the design site. It mirrors .card from components/index.html (line 3931) and adds the rhythm rules above.