CSS format has undergone three distinct evolutionary leaps over the previous eight years. Collectively, CSS Grid, Subgrid, and Container Queries substitute JavaScript-dependent format workarounds with pure CSS. This tutorial walks by means of constructing a responsive card grid element that makes use of all three options, first in plain HTML and CSS, then built-in right into a React element.
Desk of Contents
The Format Trilogy Is Lastly Full
CSS format has undergone three distinct evolutionary leaps over the previous eight years. CSS Grid landed in browsers in 2017, giving builders a real two-dimensional format engine for the primary time. Subgrid adopted, with Firefox delivery help in model 71 (December 2019) and Chrome and Edge including help in model 117 (August 2023). As soon as all main browsers supported the characteristic, Subgrid reached Baseline Newly Accessible standing in late 2023. Container queries, together with container-type, @container guidelines, and the related cqw/cqh/cqi/cqb items, reached full cross-browser stability (Chrome 105, Firefox 110, Safari 16, Edge 105), finishing what can moderately be referred to as the format trilogy.
Collectively, these three options substitute JavaScript-dependent format workarounds with pure CSS. Grid handles macro format. Subgrid enforces alignment consistency throughout sibling elements. Container queries make particular person elements aware of their very own container fairly than the viewport. Layouts that beforehand demanded JavaScript resize observers or fragile media question workarounds can now dwell totally in stylesheets.
Grid handles macro format. Subgrid enforces alignment consistency throughout sibling elements. Container queries make particular person elements aware of their very own container fairly than the viewport.
This tutorial walks by means of constructing a responsive card grid element that makes use of all three options, first in plain HTML and CSS, then built-in right into a React element. Intermediate CSS information is assumed. Grid fundamentals will get a short refresher, not a ground-up introduction.
Fast Refresher: The place Every Function Matches
CSS Grid: The Macro Format Engine
CSS Grid supplies two-dimensional management over web page and part format by means of properties like grid-template-columns, grid-template-rows, and hole. It handles the outermost structural considerations: what number of columns a format makes use of, how tracks dimension themselves relative to obtainable house, and the way objects movement into implicit or specific tracks. Grid is the muse the opposite two options construct on.
Subgrid: Aligning Kids to a Father or mother Grid
A well-recognized drawback exhibits up the second you nest grids: playing cards in a row with totally different content material lengths find yourself with misaligned headings, physique sections, and footers. By default, a grid merchandise that’s itself a grid container sizes its inner tracks independently of the mum or dad. Subgrid fixes this. The important thing syntax is grid-template-columns: subgrid and grid-template-rows: subgrid, which inform the kid to undertake the mum or dad’s column or row tracks fairly than defining its personal.
Container Queries: Part-Degree Responsiveness
Media queries reply to the viewport. Container queries reply to the dimensions of a mum or dad container, making elements actually self-contained and context-aware. The core syntax entails declaring a containment context with container-type (usually inline-size), optionally naming it with container-name, after which writing @container guidelines that fireside primarily based on the container’s dimensions. Container question items (cqw, cqh, cqi, cqb) permit fluid sizing relative to the container fairly than the viewport.
Browser Assist in 2025: The Inexperienced Board
Present Assist Matrix
| Function | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| CSS Grid | 57+ | 52+ | 10.1+ | 16+ |
| Subgrid | 117+ | 71+ | 16+ | 117+ |
| Container Queries | 105+ | 110+ | 16+ | 105+ |
| CQ Items (cqw/cqh) | 105+ | 110+ | 16+ | 105+ |
Be aware that Firefox shipped Subgrid considerably earlier (v71, December 2019) than Chrome and Edge (v117, August 2023). The “Baseline 2023” designation displays the purpose when all main engines supported the characteristic, not any single engine’s ship date.
For any mission focusing on the final two variations of main browsers, no polyfills are wanted for any of those options. All the trilogy is inexperienced throughout the board.
For groups that also want a progressive enhancement security internet, @helps requires no exterior dependencies and degrades silently in unsupporting browsers. Wrapping subgrid or container question guidelines in @helps (grid-template-rows: subgrid) or @helps (container-type: inline-size) blocks ensures sleek fallback with out counting on third-party polyfill scripts. Be aware that @helps detects particular person characteristic availability however can’t detect interplay points between mixed options. Take a look at the mixed implementation in your goal browsers.
Constructing the Trilogy Card Grid: Step by Step
Step 1: Setting Up the Outer Grid
The outer grid handles macro format, figuring out what number of playing cards seem per row and the way they reflow because the viewport adjustments. The repeat(auto-fill, minmax(280px, 1fr)) sample creates responsive columns with none media queries: columns are not less than 280px broad, broaden to fill obtainable house, and the browser creates as many as will match.
Change image-1.jpg, image-2.jpg, and image-3.jpg with actual picture paths or a placeholder service corresponding to https://picsum.photographs/400/200.
<div class="card-grid">
<article class="card">
<div class="card-inner">
<img src="image-1.jpg" alt="Function one" />
<h3>Card Heading Oneh3>
<p>Brief physique textual content for this card.p>
<a href="#" class="cta">Study Extraa>
div>
article>
<article class="card">
<div class="card-inner">
<img src="image-2.jpg" alt="Function two" />
<h3>Card Heading Twoh3>
<p>This card has considerably extra physique textual content to show alignment challenges throughout siblings.p>
<a href="#" class="cta">Study Extraa>
div>
article>
<article class="card">
<div class="card-inner">
<img src="image-3.jpg" alt="Function three" />
<h3>Card Heading Threeh3>
<p>Medium size textual content right here.p>
<a href="#" class="cta">Study Extraa>
div>
article>
div>
.card-grid {
--card-rows: 4;
show: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
hole: 1.5rem;
padding: 1.5rem;
}
This offers a completely responsive macro format with implicit monitor creation. Playing cards reflow from three columns on broad screens all the way down to a single column on slender ones, all with out breakpoints.
Step 2: Including Subgrid for Constant Card Internals
With out subgrid, every card’s inner rows are sized independently. A card with a brief heading and a card with a two-line heading may have their physique textual content and CTAs at totally different vertical positions. This misalignment is without doubt one of the commonest visible bugs in card-based layouts.
The repair: make every card a grid merchandise that spans an outlined variety of rows, then apply grid-template-rows: subgrid so its inner parts lock to the mum or dad grid’s row tracks.
Necessary: container-type should not be utilized to the identical factor that makes use of grid-template-rows: subgrid. Making use of container-type to a subgrid participant establishes dimension containment on that factor, which prevents the mum or dad grid’s row monitor sizing from propagating into the kid, breaking subgrid alignment totally. As an alternative, place container-type on an interior youngster factor (.card-inner on this tutorial) to protect subgrid monitor inheritance. This architectural alternative is mirrored within the HTML construction above.
.card {
show: grid;
grid-row: span var(--card-rows);
grid-template-rows: subgrid;
border: 1px stable var(--card-border-color, #e0e0e0);
border-radius: 8px;
overflow: hidden;
}
.card-inner {
show: grid;
grid-row: 1 / -1;
grid-template-rows: subgrid;
}
.card img {
width: 100%;
peak: 200px;
object-fit: cowl;
}
.card h3 {
padding: 0 1rem;
margin: 0;
}
.card p {
padding: 0 1rem;
margin: 0;
}
.card .cta {
padding: 0 1rem 1rem;
align-self: finish;
}
The grid-row: span var(--card-rows) declaration is crucial. It tells the mum or dad grid that every card occupies 4 row tracks (picture, heading, physique, CTA). The --card-rows customized property lives on .card-grid so the span worth and HTML youngster depend keep coupled in a single place. Then grid-template-rows: subgrid distributes the cardboard’s youngsters throughout these 4 parent-defined tracks. All sibling playing cards share the identical monitor sizing, so headings align with headings, physique textual content with physique textual content, and CTAs sit at a constant baseline no matter content material size. The span worth should match the variety of direct grid youngsters in every card. Playing cards with totally different youngster counts will misalign.
Be aware: When grid-template-rows: subgrid is energetic, the kid factor’s row-gap is ignored. Spacing between card sections is managed by the mum or dad .card-grid hole worth. Use padding on particular person card youngsters when you want totally different inner spacing.
Step 3: Making Playing cards Container-Question-Conscious
With the macro grid and subgrid alignment in place, container queries add the ultimate layer: every card can adapt its personal inner format primarily based on how a lot house it really occupies. The container-type declaration goes on .card-inner, not on .card itself, to keep away from breaking subgrid monitor inheritance.
.card-inner {
container-type: inline-size;
container-name: card;
}
@container card (min-inline-size: 480px) {
.card-inner {
grid-template-columns: 200px 1fr;
}
.card-inner img {
peak: 100%;
grid-row: 1 / -1;
}
.card-inner h3 {
font-size: clamp(1.25rem, 3cqi, 2rem);
}
}
@container card (max-inline-size: 479.999px) {
.card-inner h3 {
font-size: clamp(1rem, 5cqi, 1.5rem);
}
}
When a card’s inline dimension exceeds 480px (which occurs when the outer grid provides it sufficient room), the format switches from a vertical stack to a horizontal association with the picture on the left and textual content on the fitting. The cqi unit sizes the heading relative to the cardboard container’s inline dimension, protecting typography proportional. Each the slender and broad guidelines use clamp() to make sure headings have a minimal flooring and most ceiling, stopping accessibility points at excessive container sizes. The max-inline-size: 479.999px worth ensures there isn’t a fractional-pixel hole between the 2 breakpoints.
Utilizing container-name: card makes the question specific about which containment context it targets. This turns into important when containers are nested, stopping queries from by accident matching an ancestor container.
Step 4: The Mixed Consequence
Right here is the whole, unified implementation combining all three options:
<div class="card-grid">
<article class="card">
<div class="card-inner">
<img src="image-1.jpg" alt="Function one" />
<h3>Card Heading Oneh3>
<p>Brief physique textual content for this card.p>
<a href="#" class="cta">Study Extraa>
div>
article>
<article class="card">
<div class="card-inner">
<img src="image-2.jpg" alt="Function two" />
<h3>Card Heading Twoh3>
<p>This card has considerably extra physique textual content to show alignment challenges throughout siblings.p>
<a href="#" class="cta">Study Extraa>
div>
article>
<article class="card">
<div class="card-inner">
<img src="image-3.jpg" alt="Function three" />
<h3>Card Heading Threeh3>
<p>Medium size textual content right here.p>
<a href="#" class="cta">Study Extraa>
div>
article>
div>
.card-grid {
--card-rows: 4;
show: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
hole: 1.5rem;
padding: 1.5rem;
}
.card {
show: grid;
grid-row: span var(--card-rows);
grid-template-rows: subgrid;
border: 1px stable var(--card-border-color, #e0e0e0);
border-radius: 8px;
overflow: hidden;
}
.card-inner {
show: grid;
grid-row: 1 / -1;
grid-template-rows: subgrid;
container-type: inline-size;
container-name: card;
}
.card img {
width: 100%;
peak: 200px;
object-fit: cowl;
}
.card h3 {
padding: 0 1rem;
margin: 0;
font-size: clamp(1rem, 4vw, 1.5rem);
}
.card p {
padding: 0 1rem;
margin: 0;
}
.card .cta {
padding: 0 1rem 1rem;
align-self: finish;
}
@container card (min-inline-size: 480px) {
.card-inner {
grid-template-columns: 200px 1fr;
}
.card-inner img {
peak: 100%;
grid-row: 1 / -1;
}
.card-inner h3 {
font-size: clamp(1.25rem, 3cqi, 2rem);
}
}
@container card (max-inline-size: 479.999px) {
.card-inner h3 {
font-size: clamp(1rem, 5cqi, 1.5rem);
}
}
Resizing the browser triggers a cascade: the outer grid reflows its columns, which adjustments every card’s obtainable inline dimension, which fires the container queries, which restructure the cardboard’s internals, whereas subgrid alignment holds regular throughout sibling playing cards all through all the course of.
Resizing the browser triggers a cascade: the outer grid reflows its columns, which adjustments every card’s obtainable inline dimension, which fires the container queries, which restructure the cardboard’s internals, whereas subgrid alignment holds regular throughout sibling playing cards all through all the course of.
The bottom .card h3 font-size makes use of viewport-relative vw items fairly than cqi items. The cqi unit is simply legitimate inside @container blocks the place the containment context is assured to be established. Outdoors these blocks, cqi could resolve unpredictably. The @container guidelines override the bottom worth with cqi-based sizing as soon as containment is confirmed.
Integrating the Trilogy in a React Part
Part Structure
The React implementation makes use of a CardGrid wrapper element and a Card youngster element. The crucial architectural choice: all format logic lives in CSS. React handles knowledge mapping and rendering solely. No JavaScript-based resize observers are wanted for the responsive conduct.
This implementation requires React 17+ with the automated JSX remodel enabled. For React variations beneath 17, add import React from 'react'; on the prime of the element file. CSS Modules help is required out of your bundler (Vite, Subsequent.js, and Create React App all help this out of the field).
When You Nonetheless Want JavaScript
Container queries eradicate most resize-observer use circumstances for format, however edge circumstances stay. Studying precise container dimensions for animation thresholds, triggering analytics occasions at particular sizes, or integrating with third-party libraries that require pixel values nonetheless requires ResizeObserver. It enhances container queries fairly than changing them.
React Code Walkthrough
import kinds from './CardGrid.module.css';
perform sanitizeUrl(url) {
if (!url) return '#';
const trimmed = url.trim().toLowerCase();
if (trimmed.startsWith('javascript:') || trimmed.startsWith('knowledge:')) {
return '#';
}
return url;
}
perform Card({ picture, alt, title, physique, ctaUrl, ctaText }) {
return (
<article className={kinds.card}>
<div className={kinds.cardInner}>
<img src={picture} alt={alt ?? ''} />
<h3>{title}h3>
<p>{physique}p>
<a href={sanitizeUrl(ctaUrl)} className={kinds.cta}>{ctaText}a>
div>
article>
);
}
export default perform CardGrid({ playing cards }) {
return (
<div className={kinds.cardGrid}>
{playing cards.map((card, index) => {
const stableKey = card.id != null ? card.id : `card-fallback-${index}`;
if (card.id == null && course of.env.NODE_ENV !== 'manufacturing') {
console.warn('CardGrid: card at index', index, 'is lacking a secure `id` prop.');
}
return <Card key={stableKey} {...card} />;
})}
div>
);
}
Use a secure distinctive identifier out of your knowledge because the key prop. Index keys trigger reconciliation errors when playing cards are reordered, filtered, or eliminated. The element will warn in growth if a card is lacking an id.
.cardGrid {
--card-rows: 4;
show: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
hole: 1.5rem;
padding: 1.5rem;
}
.card {
show: grid;
grid-row: span var(--card-rows);
grid-template-rows: subgrid;
border: 1px stable var(--card-border-color, #e0e0e0);
border-radius: 8px;
overflow: hidden;
}
.cardInner {
show: grid;
grid-row: 1 / -1;
grid-template-rows: subgrid;
container-type: inline-size;
container-name: card;
}
.card img {
width: 100%;
peak: 200px;
object-fit: cowl;
}
.card h3 {
padding: 0 1rem;
margin: 0;
font-size: clamp(1rem, 4vw, 1.5rem);
}
.card p {
padding: 0 1rem;
margin: 0;
}
.cta {
padding: 0 1rem 1rem;
align-self: finish;
}
@container card (min-inline-size: 480px) {
.cardInner {
grid-template-columns: 200px 1fr;
}
.cardInner img {
peak: 100%;
grid-row: 1 / -1;
}
.cardInner h3 {
font-size: clamp(1.25rem, 3cqi, 2rem);
}
}
@container card (max-inline-size: 479.999px) {
.cardInner h3 {
font-size: clamp(1rem, 5cqi, 1.5rem);
}
}
React owns the info movement; CSS owns each responsive conduct. No resize handlers run in JavaScript, and each format change occurs by means of CSS alone. One sensible word on tooling: Tailwind CSS v3.2+ helps container queries through the @tailwindcss/container-queries plugin; Tailwind v4 contains native help. Subgrid utilities stay restricted as of Tailwind v4.x, so confirm present protection to your model.
Superior Patterns and Gotchas
Nesting Container Queries Inside Subgrid Objects
A subgridded card can itself include a nested element, like a media object, that makes use of its personal @container guidelines. This works, however requires consciousness of two constraints:
container-typeand subgrid can’t coexist on the identical factor.container-type: inline-sizeestablishes dimension containment, which creates a format boundary that blocks the mum or dad grid’s monitor sizes from reaching the contained factor. In the event you want each subgrid and container queries, placecontainer-typeon an interior wrapper factor and maintaingrid-template-rows: subgridon the outer grid participant.container-type: inline-sizeestablishes a brand new block formatting context on the factor it is utilized to. Little one margins not collapse by means of the containment boundary, and overflow defaults toseenthroughout the new BFC. Account for each behaviors when nesting containment inside subgrid buildings.
Efficiency Concerns
Container queries introduce a format dependency chain: the browser sizes the container, then evaluates the question and lays out the container’s youngsters. At three or fewer nesting ranges, this provides no seen body value (verified in Chrome DevTools Efficiency panel). Past that depth, every extra containment context provides format recalculation passes. Profile with the DevTools Efficiency panel to verify whether or not deeper nesting impacts your particular format. Subgrid reuses the mum or dad’s already-computed monitor sizing and customarily doesn’t add significant format value in comparison with common grid.
Frequent Errors
Three errors seem repeatedly when builders first mix these options. Forgetting to declare container-type on the container factor is the commonest: with out it, @container guidelines silently do nothing. No console warnings, no errors, simply kinds that by no means apply. One other frequent mistake is utilizing subgrid with out setting span on the mum or dad grid merchandise. With out an specific span, the merchandise occupies a single monitor, so subgrid has nothing helpful to inherit, and the format collapses. The third is declaring container-type: dimension (which incorporates each block and inline dimensions) when solely inline-size containment is required. The dimension worth forces the browser to include the factor’s peak as nicely, which may produce surprising format outcomes when the factor’s peak must be decided by its content material.
Forgetting to declare
container-typeon the container factor is the commonest: with out it,@containerguidelines silently do nothing. No console warnings, no errors, simply kinds that by no means apply.
Implementation Guidelines
- Outline macro grid on the outermost wrapper
- Set
grid-row: span Non playing cards matching inner content material sections (N should equal the variety of direct grid youngsters) - Apply
grid-template-rows: subgridon every card - Add
container-type: inline-sizeto an interior wrapper factor (not the subgrid participant itself) - Identify containers with
container-namewhen nesting (use distinctive names per element to keep away from world collisions) - Write
@containerbreakpoints primarily based on element widths, not viewport - Use
cqi/cqbitems solely inside@containerblocks the place containment is established; use viewport-relative items (vw) for base kinds outdoors container queries - Use
clamp()withcqiitems to set minimal and most bounds for fluid inner sizing - Take a look at with browser DevTools container question overlay: Chrome DevTools (Components → Format → Container Queries, Chrome 105+) or Firefox Inspector (Grid/Flexbox overlay panel, Firefox 110+)
- Validate with
@helpsfor any legacy viewers, and complement with purposeful testing for the mixed characteristic implementation - Audit nesting depth and maintain containment at three ranges or fewer
Ship It With out a Polyfill
The format trilogy of CSS Grid, Subgrid, and Container Queries is production-ready in each main browser (Chrome, Firefox, Safari, and Edge). No polyfills or JavaScript fallbacks are required. Evaluate the Superior Patterns part for identified gotchas earlier than delivery. The CSS Containment Degree 3 and CSS Grid Degree 2 specs present the authoritative references for edge circumstances and future additions.
