Building card layouts and hero sections in CSS comes down to naming the parts, because these two patterns appear on nearly every landing page and they get copied badly. Treat them as a fixed set of parts with names and both become reusable.
What actually makes a thing a card?
A card is a surface with a border or a shadow that groups related content into one clickable or readable unit. The visual treatment is optional; the grouping is the point. Once you can name the parts, the styles stop being a pile of one-off rules.
- Media — an image, a thumbnail or a short loop that identifies the item.
- Body — the container that holds the title, the description and any metadata.
- Title — the name of the thing, usually a heading element so it is announced correctly.
- Meta — price, rating, date or category, presented as a short line.
- Action — the link or button that does the thing.
The single most useful decision is whether the whole card is the link or only the title is. If the whole card is a link, the action becomes a second link to the same destination, which produces duplicate links for screen reader users. Most teams settle on making the title the link, giving the card a hover treatment through the link, and leaving any other action as a separate button.
How do you build a row of cards that stays aligned?
A grid of equal cards is where grid earns its place. Grid items stretch to the height of their row by default, so every card in a row ends up the same height without any height juggling. A flex row would need the cards to be told to grow, and the last row would behave differently.
<section class="products" aria-labelledby="products-title">
<h2 id="products-title">Commuter kit</h2>
<ul class="product-list">
<li class="product-card product-card--featured">
<img class="product-card__media" src="pannier.jpg" alt="Waxed canvas pannier on a handlebar">
<div class="product-card__body">
<h3 class="product-card__title"><a href="/pannier">Waxed pannier</a></h3>
<p class="product-card__meta">£48.00</p>
<a class="product-card__action" href="/pannier">View details</a>
</div>
</li>
<li class="product-card">
<img class="product-card__media" src="lamp.jpg" alt="USB handlebar lamp with a bracket">
<div class="product-card__body">
<h3 class="product-card__title"><a href="/lamp">Handlebar lamp</a></h3>
<p class="product-card__meta">£22.50</p>
<a class="product-card__action" href="/lamp">View details</a>
</div>
</li>
</ul>
</section>
.product-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 1.5rem;
margin: 0;
padding: 0;
list-style: none;
}
.product-card {
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #ffffff;
}
.product-card__media {
display: block;
inline-size: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}
.product-card__body {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.5rem;
padding: 1rem;
}
.product-card__title {
margin: 0;
font-size: 1.05rem;
}
.product-card__meta {
margin: 0;
color: #475569;
}
.product-card__action {
margin-block-start: auto;
align-self: start;
}
Three details do the work here. The aspect-ratio on the image gives every thumbnail the same shape regardless of the source file, and object-fit: cover crops rather than squashes. The body is a flex column with flex: 1 so it fills whatever height the row decided on. And the action carries margin-block-start: auto, which pushes it to the bottom of the body so the buttons of several cards line up with each other even when the descriptions are different lengths.
How do you build a hero section that stays readable?
A hero is a large area at the top of the page that states what the site is and gives one clear action. The hard part is not the layout, it is the text over a photograph. A dark overlay between the image and the text is what makes the text readable whatever the photo contains.
<header class="hero">
<div class="hero__inner">
<p class="hero__eyebrow">Free delivery over £60</p>
<h1 class="hero__title">Kit for the ride to work</h1>
<p class="hero__lede">Weatherproof bags, lights and locks, chosen by people who cycle every day.</p>
<a class="hero__action" href="/shop">Shop the collection</a>
</div>
</header>
.hero {
min-block-size: 30rem;
display: grid;
align-items: center;
background-image:
linear-gradient(rgb(9 20 36 / 0.72), rgb(9 20 36 / 0.72)),
url("hero-street.jpg");
background-size: cover;
background-position: center;
color: #ffffff;
}
.hero__inner {
max-inline-size: 40rem;
margin-inline: auto;
padding: 3rem 1.25rem;
}
.hero__title {
margin: 0 0 0.75rem;
font-size: clamp(2rem, 6vw, 3.5rem);
line-height: 1.1;
}
.hero__lede {
margin: 0 0 1.5rem;
font-size: 1.125rem;
}
.hero__action {
display: inline-block;
padding: 0.75rem 1.25rem;
border-radius: 999px;
background: #ffffff;
color: #0f172a;
font-weight: 600;
}
The gradient and the photograph are two layers of the same background, and the gradient comes first so it sits on top. Two details worth copying: min-block-size rather than a fixed height, so the hero grows if the text needs more room, and clamp() for the heading, so the type scales with the viewport between a readable minimum and a sensible maximum without a single media query.
How do you keep both patterns reusable?
Reuse comes from naming, not from copying. Give every part a name that describes its role rather than its position on a particular page, and the same class works on the product page, the sale page and the search results. Prefix the names with the component so two components can both have a title without colliding, and mark the variations with a modifier rather than a second set of rules.
/* the base component */
.product-card {
border-radius: 12px;
border: 1px solid #e2e8f0;
}
/* a variation of the same component, not a second one */
.product-card--featured {
border-color: #1d4ed8;
box-shadow: 0 10px 30px rgb(15 23 42 / 0.08);
}
A different component that happens to look similar, such as a sale card with its own behaviour, gets its own block name rather than a modifier on the product card. Keep the shared look in one place. If every card in the site uses the same radius, the same border and the same padding, those values belong on the card class or on a custom property, not on four near-identical rules with slightly different numbers.
Checklist before you ship
- Every card in a row ends at the same height, with actions on the same line.
- Media has a fixed aspect ratio so a slow-loading image does not shift the layout.
- The hero text has a contrast-safe background, not just a photograph behind it.
- There is exactly one link per destination, with the title as the main one.
- Heading levels inside a card continue the document outline rather than restarting it.
- Class names describe parts, and variations are modifiers of an existing component.
- The hero has one obvious action, and it looks different from a secondary link.
Try it yourself: build the bike shop page with two cards and a hero, then shorten the second card's description so it no longer matches the first in length. Check that both action links still sit on the same baseline, which the flex column and the auto margin are meant to guarantee. Next, replace the image of the second card with one that is half as tall and confirm the row does not move, because the aspect ratio is holding the space. Finally, remove the gradient layer from the hero and judge whether the heading is still readable against the photograph.