Visual hierarchy in UI layouts
Learn how scale, contrast, and spacing guide the eye through a composition.
Home / Snippets / UI Components /
Responsive card component with a fixed-ratio image area using aspect-ratio and object-fit to keep media consistently sized regardless of source dimensions.
Learn how scale, contrast, and spacing guide the eye through a composition.
The subtle but important difference between the two repeat keywords.
Scale font sizes smoothly across any viewport without media queries.
Stick to transform and opacity to keep animations on the compositor thread.
.image-card {
background: var(--card);
border-radius: var(--radius);
overflow: hidden;
display: grid;
grid-template-rows: auto 1fr;
}
.image-card__media {
aspect-ratio: 16 / 9;
overflow: hidden;
}
.image-card__media img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.image-card__body {
padding: 1rem 1.125rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
/* Responsive grid of cards */
.image-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 1.25rem;
}
Includes role, constraints, two framework variants, and edge cases to handle.
You are a senior frontend engineer building a card component library.
Goal: A responsive image card component with a fixed-ratio image area that
never distorts, a title, description, and metadata row — no JavaScript.
Technical constraints:
- Use display: grid with grid-template-rows: auto 1fr on the card so the
body section stretches to fill available height in equal-height grids.
- Use aspect-ratio: 16 / 9 (or 4 / 3) on the image wrapper — never the
old padding-top percentage hack.
- Apply object-fit: cover and width/height: 100% on the <img> so images
fill the ratio box without distortion regardless of source dimensions.
- Use overflow: hidden on both the card and the media container to clip
images that exceed the ratio box.
- Use oklch() for all color values — no hex or rgba.
- Use CSS custom properties (var(--card), var(--text), var(--muted), etc.)
for theming so the card adapts to dark and light modes.
- The card grid should use grid-template-columns: repeat(auto-fit,
minmax(14rem, 1fr)) to reflow at any viewport without media queries.
Framework variant (pick one):
A) Vanilla HTML + CSS — semantic <article> with BEM class names
(.image-card, .image-card__media, .image-card__body, etc.).
B) React component — accept src, alt, label, title, description, and meta
props; render a semantic <article>; apply object-fit via inline style or
a CSS module.
Edge cases to handle:
- Tall or portrait images must not overflow the ratio box — object-fit: cover
handles this, but also add overflow: hidden to the media wrapper as a
safety net for browsers that apply object-fit only to replaced elements.
- In an equal-height grid row (align-items: stretch), use grid on the card
itself so the body fills remaining height, allowing a footer/meta row to
pin to the bottom with margin-top: auto.
- For cards without images, the aspect-ratio container should collapse
gracefully — either hide it with display: none or provide a placeholder
gradient background via a CSS class modifier.
Return CSS and semantic HTML only (or a React component if variant B).
Before aspect-ratio landed in browsers, developers maintained image ratios using the padding-top percentage trick: set padding-top: 56.25% (9÷16) on an empty wrapper, then absolutely position the image inside it. It worked, but it was fragile — adding actual content to the wrapper would break the ratio, and the technique relied on a CSS side-effect (percentage padding resolves against the element's width) that was never intended as a layout tool.
aspect-ratio: 16 / 9 is declarative and self-documenting. It participates in the normal flow, works with any display type, and composes cleanly with min-width, max-height, and grid sizing. The browser calculates the height automatically from the width, so the ratio is maintained whether the card is 200 px or 600 px wide.
Browser support is now universal across all modern browsers. The padding-top technique should be considered a historical curiosity — reach for aspect-ratio instead.
A raw <img> with width: 100%; height: 100% will stretch to fill its container, distorting the image if the source dimensions don't match the container's aspect ratio. object-fit: cover tells the browser to scale the image up or down until it covers the entire container, then crop any overflow — the same behaviour as background-size: cover but for replaced elements like <img> and <video>.
The key detail is that object-fit only works when the image has explicit dimensions. Apply both width: 100% and height: 100% to the <img> element, and add display: block to eliminate the inline baseline gap. Without the explicit height, object-fit: cover has nothing to cover and falls back to the default fill behaviour.
For art direction — choosing a different crop per viewport — pair object-fit with object-position. The default centers the image; object-position: top keeps faces in frame for portrait photos in landscape containers.
The card grid uses grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)). This single line creates a fully responsive grid with no media queries: at narrow viewports cards stack to a single column, and at wider viewports the grid adds columns as space allows, each at least 14rem wide.
The distinction between auto-fit and auto-fill matters here. Both create as many columns as fit the container, but auto-fit collapses empty tracks to zero width, allowing existing items to stretch to fill the row. auto-fill preserves empty column tracks, which caps items at their minmax minimum. For image cards where you want items to fill the row width, auto-fit is the right choice.
To prevent cards from growing too wide in sparse rows (e.g., a single card spanning the full grid width), add max-width to the card itself, or use justify-content: start on the grid so items don't stretch when there are fewer than a full row's worth.