The most common card design mistake isn't in any individual card — it's in the grid. A designer polishes one card in isolation: nice padding, a subtle shadow, decent typography. Then twelve of them sit next to each other in a grid and the whole thing reads as visual noise, because nothing differentiates hover state from rest state, nothing establishes a hierarchy between cards, and the shadow that looked subtle on one card looks like a muddy grey wash multiplied across a dozen.
Strip away the styling and every card is really four zones, and getting the spacing relationship between them right matters more than any color choice:
<article class="card">
<img class="card-media" src="thumb.jpg" alt="">
<div class="card-body">
<span class="card-eyebrow">Product</span>
<h3 class="card-title">Wireless Keyboard</h3>
<p class="card-desc">Low-latency, backlit, 40-day battery.</p>
<div class="card-footer">
<span class="card-price">$89</span>
<a href="#" class="card-cta">View →</a>
</div>
</div>
</article>
A single large box-shadow tends to look like a grey smudge rather than actual elevation, because real-world shadows aren't uniform — they have a tight, dark contact shadow close to the object and a soft, diffuse shadow further out. Stacking two shadow values recreates that:
.card {
border-radius: 14px;
box-shadow:
0 1px 2px rgba(0,0,0,0.24),
0 8px 24px rgba(0,0,0,0.12);
transition: box-shadow 200ms ease, transform 200ms ease;
}
.card:hover {
box-shadow:
0 2px 4px rgba(0,0,0,0.28),
0 16px 40px rgba(0,0,0,0.18);
transform: translateY(-4px);
}
The small first shadow keeps the card grounded; the larger second shadow is what actually communicates "lifted" on hover. Moving the card up 4px with translateY at the same time reinforces the shadow change rather than fighting it.
Cards with variable-length titles and descriptions naturally produce uneven heights in a grid, which looks unintentional rather than dynamic. Flexbox on the card, combined with flex-grow on the body, keeps footers aligned across a row regardless of content length:
.card {
display: flex;
flex-direction: column;
height: 100%;
}
.card-body {
flex: 1;
display: flex;
flex-direction: column;
}
.card-footer {
margin-top: auto; /* pushes footer to the bottom regardless of body length */
}
UIXDraft's 180+ template bundle includes card patterns for pricing, blog, product, and dashboard layouts with elevation and hover states already tuned — copy and restyle.
Browse the templates →A card description that overflows its box breaks the layout; naive overflow: hidden chops text off wherever it happens to land. -webkit-line-clamp (now supported broadly enough to use without a fallback for most audiences) truncates cleanly at a line boundary and adds an ellipsis automatically:
.card-desc {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
A card whose media area resizes once its image finishes loading causes a visible layout jump — the exact kind of shift that hurts both perceived polish and Core Web Vitals' Cumulative Layout Shift score. Reserving the space with aspect-ratio before the image ever arrives avoids it entirely:
.card-media {
aspect-ratio: 16 / 9;
object-fit: cover;
width: 100%;
}
The browser now allocates the correct height immediately, even while the image is still downloading, so surrounding content — the card body, the cards next to it in a grid — never has to reflow once the image finally paints.
A blog card's job is to earn a click; a pricing card's job is to make one plan visually win without shouting. The usual pattern is a border and slight scale increase on the "recommended" tier, not a louder color:
.pricing-card.featured {
border: 2px solid var(--purple);
transform: scale(1.04);
position: relative;
}
.pricing-card.featured::before {
content: "Most Popular";
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: var(--purple);
color: #06080f;
font-size: 12px;
font-weight: 700;
padding: 4px 12px;
border-radius: 999px;
}
Removing the default focus outline with outline: none is one of the most common accessibility regressions in card design — it makes the card invisible to keyboard and switch-device users trying to navigate a grid without a mouse. Rather than deleting the outline, replace it with something that matches the card's visual language:
.card:focus-visible {
outline: 2px solid var(--purple);
outline-offset: 3px;
}
.card:focus:not(:focus-visible) {
outline: none; /* mouse clicks don't need the ring */
}
:focus-visible is the key detail — it shows the outline for keyboard navigation but suppresses it for mouse clicks, so you get accessible focus indication without the "outline flashing on every click" complaint that made developers remove outlines in the first place.
A card grid that renders empty white boxes while data loads reads as broken; a skeleton that mimics the eventual layout reads as intentional and fast, even at identical load time. A simple shimmer needs only a gradient and one keyframe animation:
.skeleton {
background: linear-gradient(90deg, #1a1f2b 25%, #232936 37%, #1a1f2b 63%);
background-size: 400% 100%;
animation: shimmer 1.4s ease infinite;
border-radius: 8px;
height: 16px;
}
@keyframes shimmer {
0% { background-position: 100% 50%; }
100% { background-position: 0 50%; }
}
Size individual skeleton blocks to roughly match the real content's dimensions (a wider block for the title, a narrower one for the eyebrow label) so the transition from skeleton to real content doesn't cause a layout jump.
Even with well-built individual cards, a grid can still feel lifeless. The usual causes: every card using the exact same shadow strength regardless of content importance, no hover feedback at all (a card that doesn't visually respond to interaction reads as non-interactive even if it's a link), and inconsistent internal padding that makes the grid feel unaligned even though the outer card sizes match.
If a card only leads to one destination, make the whole card clickable — wrap it in an anchor or add a full-card overlay link, since a small text-only click target is a common source of missed clicks, especially on mobile. If a card has multiple actions (e.g., a product card with both "view" and "add to cart"), keep those as separate, clearly distinct targets instead.
There's no universal rule, but 8–16px reads as modern and approachable without looking like a mobile app widget; anything past ~20px starts to feel bubbly on a desktop-sized card. Match the radius to nested elements too — an image inside a 12px-radius card should use a slightly smaller radius (e.g. 8px) so the corners appear concentric rather than mismatched.
Overlapping shadow color at high opacity compounds visually when cards sit close together, especially in a dense grid. Lower the shadow opacity (try 0.08–0.15 for the diffuse layer) and lean on the tighter contact shadow for definition instead of a single heavy blur. It also helps to check the shadow against the actual page background rather than a white canvas — a shadow tuned in isolation almost always reads darker once it's sitting against a real, busier layout.