CSSCard Patterns

Card Patterns

Cards are the workhorse component of modern web UI — product tiles, articles, profile summaries, pricing plans. They look simple, but a production-ready card has to handle variable content length, images that may not exist, hover and focus states, and a grid of cards that all need to line up despite different amounts of text. This page builds up from the simplest card to a full responsive card grid.

The basic card

A card is a padded box with a border or shadow that visually groups related content. That is the entire concept — everything else is refinement.

HTML
<div class="card">
  <h3 class="card-title">Card title</h3>
  <p class="card-body">
    Some supporting text that describes the card's content in a sentence
    or two.
  </p>
  <a href="#" class="card-link">Read more</a>
</div>

CSS
.card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 20px;
  border-radius: 12px;
  background: white;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
}

.card-title {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 600;
}

.card-body {
  margin: 0;
  color: #555;
  line-height: 1.5;
}

.card-link {
  margin-top: auto;
  font-weight: 600;
  color: #2563eb;
  text-decoration: none;
}
Why margin-top: auto on the link
Flexbox with flex-direction: column lets you push a single child to the bottom by giving it margin-top: auto. That is what keeps the "Read more" link flush with the card's bottom edge even when the body text is short.
Image card

Adding an image is where cards start to break in practice — inconsistent image dimensions cause uneven cards. Constrain the image with a fixed aspect-ratio and object-fit so the card shape stays predictable regardless of the source image size.

HTML
<article class="card card--image">
  <img class="card-image" src="/thumb.jpg" alt="Descriptive alt text" />
  <div class="card-content">
    <h3 class="card-title">Article title</h3>
    <p class="card-body">One or two lines of summary text go here.</p>
  </div>
</article>

CSS
.card--image {
  padding: 0;
  overflow: hidden;
}

.card-image {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
}

.card-content {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
aspect-ratio over fixed height
Use aspect-ratio instead of a fixed height on the image. A fixed height breaks on unusual image proportions;aspect-ratio combined with object-fit: cover keeps the crop sensible no matter the source dimensions.
Horizontal card

Search results and list-style layouts often need the image beside the text rather than above it. Flexbox on the row axis handles this directly.

CSS
.card--horizontal {
  display: flex;
  flex-direction: row;
  padding: 0;
  overflow: hidden;
}

.card--horizontal .card-image {
  width: 140px;
  aspect-ratio: 1;
  flex-shrink: 0;
}

.card--horizontal .card-content {
  padding: 16px;
  min-width: 0; /* allow text to truncate instead of overflowing */
}

.card--horizontal .card-body {
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
min-width: 0 is not optional
Inside a flex row, a text child defaults to a minimum width equal to its content's longest unbreakable string, which can force the row to overflow. Setting min-width: 0 on the flex child that holds the text lets it shrink and wrap or truncate properly.
Clickable card (the pseudo-element trick)

Making an entire card clickable while keeping exactly one real link tag (good for accessibility and SEO) is done with a "stretched link" — ::after positioned to cover the whole card, linking through the parent's position: relative.

HTML
<article class="card card--clickable">
  <h3 class="card-title">Card title</h3>
  <p class="card-body">
    Only this specific link is a real link; the whole card becomes
    clickable through the pseudo-element below.
  </p>
  <a href="/details" class="card-stretched-link">View details</a>
</article>

CSS
.card--clickable {
  position: relative;
  transition: box-shadow 150ms ease, transform 150ms ease;
}

.card-stretched-link::after {
  content: "";
  position: absolute;
  inset: 0; /* top/right/bottom/left: 0 in one declaration */
}

.card--clickable:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
  transform: translateY(-2px);
}

.card--clickable:has(.card-stretched-link:focus-visible) {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}
Why not just wrap everything in <a>
Wrapping the entire card in a single <a> is invalid when the card also contains other interactive elements (a secondary "Share" button, for example) and it also makes every word of the card read as one giant, confusing link for screen readers. The pseudo-element trick keeps one specific, well-labeled link while still making the whole card clickable visually.
Card grid with equal heights

Grid, not flexbox, is the right tool when you need a row of cards to share equal height automatically — grid rows stretch children to the tallest item in the row by default.

CSS
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 24px;
  align-items: stretch; /* default, but explicit for clarity */
}

.card-grid .card {
  height: 100%; /* fill the stretched grid cell */
}

Layout

Equal-height cards?

Notes

Flexbox row

Yes, by default

Cross-axis stretch is the flex default

Grid

Yes, by default

align-items: stretch is the grid default too

Floats

No

Historically needed a "clearfix" and equal-height hacks

Hover and focus effects

CSS
.card {
  transition: transform 200ms ease, box-shadow 200ms ease;
}

.card:hover,
.card:focus-within {
  transform: translateY(-4px);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}

@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
  }
}
focus-within, not just hover
Always pair :hover with :focus-within. Keyboard users tabbing into the card's link never trigger:hover, so without :focus-within they get no visual feedback that the card is active.
Accessibility checklist
  • Every image needs meaningful alt text, or alt="" if it is purely decorative

  • Only one real, clearly-labeled link or button per card — avoid nested interactive elements

  • Ensure sufficient color contrast between card text and its background, especially on image overlays

  • Add a visible focus style (:focus-visible) — do not rely on hover-only affordances

  • Keep heading levels consistent inside a repeated card grid (all h3, for example) so the page outline stays logical

Next
Learn how these cards fit into a full grid layout in Grid Patterns & Use Cases.