CSS in 2026: Subgrid, Container Queries, and @layer
Modern CSS layout and organization in 2026 — grid subgrid for nested alignment, container queries for component-driven responsive design, and @layer for cascade management.
Published on • August 14, 2026
AI Assistant

Every CSS feature starts as a workaround. You want a card grid where every card’s button sits at the same height, so you hard-code min-height: 320px and pray the copy doesn’t change. You want a component to respond to the width of the card it’s placed in, not the viewport, so you reach for a dozen @media breakpoints. You want your reset to not fight your design system, so you start counting !importants. In 2026 all three workarounds have a native answer: subgrid, container queries, and cascade layers — and all three are Baseline “widely available” in every modern browser.
In this tutorial, you will learn what problem each feature solves, then build toward a real, component-driven example that uses subgrid to align nested content across a card grid, container queries to make cards respond to their own size, and @layer to keep framework, component, and utility styles from fighting each other.
Key technologies: CSS Grid Level 2 subgrid, @container queries with container units, and the CSS Cascading and Inheritance Level 5 @layer rule.
Prerequisites
- Comfort with CSS Grid basics —
display: grid,grid-template-columns,grid-template-rows, and track sizing. - A browser released after 2023 for full support of subgrid, container queries, and
@layer— any current Chrome, Edge, Firefox, or Safari works. - A blank HTML page and your browser’s DevTools grid inspector (useful for visualizing subgrid tracks).
Subgrid: nested grids that share the parent’s tracks
The problem: when you nest a grid inside a grid item, the nested grid starts fresh. Its tracks are sized independently of the parent, so columns and rows inside your cards never line up with the columns and rows of cards next to them.
The fix: the subgrid value for grid-template-columns and grid-template-rows. Instead of defining new tracks, the nested grid inherits the tracks of its parent. A subgrid that spans three parent columns gets three columns sized exactly like the parent’s.
.grid {
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(4, minmax(100px, auto));
}
.item {
display: grid;
grid-column: 2 / 7; /* spans 5 parent columns */
grid-row: 2 / 4; /* spans 2 parent rows */
grid-template-columns: subgrid; /* 5 tracks, sized by the parent */
grid-template-rows: subgrid; /* 2 tracks, sized by the parent */
}
Because .item is a subgrid, its children can be placed on the outer grid’s lines — a grandchild of .grid aligns perfectly with other grid items, even though it isn’t a direct child.
Two details worth knowing from the spec:
- Line numbers restart inside the subgrid. Column line 1 inside the subgrid is the first line of the subgrid, not of the parent. This means a component can be dropped anywhere on the main grid and its internal line numbers stay predictable.
- Gaps are inherited. The parent’s
gapcarries into the subgrid, but you can override it — settingrow-gap: 0on the subgrid returns the space to its items.
.item {
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
row-gap: 0; /* the parent gap was 20px; tighten it here */
}
Support: subgrid has been available across browsers since September 2023 — you can ship it without a fallback.
Container queries: respond to the container, not the viewport
The problem: @media queries answer one question — “how big is the viewport?” But a dashboard widget doesn’t care about the viewport; it cares about the width of the card it lives in. Two identical components in a sidebar and a main column need different layouts, and viewport breakpoints can’t tell them apart.
The fix: container queries. First declare a containment context with container-type, then write @container queries that respond to that container’s size.
.post {
container-type: inline-size; /* queryable by inline (width) size */
}
Now any descendant can query that container:
.card h2 {
font-size: 1em;
}
/* when the container is wider than 700px */
@container (width > 700px) {
.card h2 {
font-size: 2em;
}
}
The same .card now scales based on where it’s placed — narrow container, small title; wide container, large title. No viewport math involved.
Naming containers and the shorthand
When multiple containers are nested, name the one you want to target with container-name. The container shorthand declares name and type in one line:
.sidebar {
container: sidebar / inline-size;
}
@container sidebar (width > 700px) {
.card {
font-size: 2em;
}
}
You can even query a container by name alone (no size condition) — a name-only query applies styles only to descendants of that named container:
@container my-container {
p {
background-color: lime;
}
}
Container query units
Inside a size query you get units relative to the query container, letting a component scale fluidly across placements:
cqw— 1% of the container’s widthcqh— 1% of the container’s heightcqi— 1% of the container’s inline sizecqb— 1% of the container’s block sizecqmin/cqmax— the smaller / larger ofcqiandcqb
@container (width > 700px) {
.card h2 {
font-size: max(1.5em, 1.23em + 2cqi);
}
}
If no container is eligible, container units fall back to the small viewport unit for that axis. Support: container size queries are widely available (Baseline since 2023); style and scroll-state queries are newer but progressively supported.
@layer: declare the cascade order once
The problem: specificity wars. Your reset, the framework, your design system, and a utility class all compete to style .button. The winner is whichever selector happens to be more specific, not whichever is more important — so you end up with .card .button.btn.btn--primary:hover monstrosities and !important as a debugging tool.
The fix: cascade layers. Declare the order of your layers once with a statement rule, then assign rules to layers. The last layer wins regardless of specificity:
@layer reset, framework, components, utilities;
Rules declared inside later layers override earlier ones — even when the earlier rule has higher specificity. Utilities, as the last layer, always win over components:
@layer framework {
.btn {
background: blue;
}
}
@layer utilities {
.bg-red {
background: red;
}
}
Here .bg-red wins over .btn even though both are single-class selectors, because utilities comes after framework.
Key rules of the game:
- Styles outside any layer always beat layered styles. Unlayered author CSS is treated as a final, unnamed layer with top priority. Put genuinely global one-offs there deliberately.
!importantinverts the order. Among!importantdeclarations, the first declared layer wins.- Layers can be nested with
@layer framework { @layer layout {} }, and you can append to a nested layer later with the dotted name@layer framework.layout. - Import layers via
@import "theme.css" layer(theme);—@importmust come before all other rules except@charsetand@layerstatements.
/* Statement: establish order once, at the top of the file */
@layer reset, base, components, utilities;
/* Anonymous layer: order is "when declared", and it can't be added to later */
@layer {
body { margin: 0; }
}
Support: @layer has been available across browsers since March 2022.
Combining all three: a component-driven card grid
Now let’s put them together. The goal: a reusable .metric-card that (1) aligns its inner rows with its neighbors using subgrid, (2) reflows based on the width of its own column using container queries, and (3) lives in an ordered set of layers so the design system can’t be overridden by accident.
<div class="metrics">
<article class="metric-card">
<h2 class="metric-label">Monthly revenue</h2>
<p class="metric-value">$42,580</p>
<p class="metric-change">+12.4% vs last month</p>
<a class="metric-link" href="/revenue">View report</a>
</article>
<article class="metric-card">
<h2 class="metric-label">Active users</h2>
<p class="metric-value">8,912</p>
<p class="metric-change">+3.1% vs last month</p>
<a class="metric-link" href="/users">View report</a>
</article>
</div>
Step 1 — @layer: order the cascade first. Everything below is a cascade layer, so a later utilities override always wins cleanly:
@layer reset, base, components, utilities;
@layer components {
.metric-card {
display: grid;
gap: 8px;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 1rem;
}
.metric-value {
font-size: 1.75rem;
font-weight: 700;
}
.metric-link {
align-self: end;
}
}
Step 2 — subgrid: make the inner rows line up across cards. The outer .metrics grid defines the rows; each .metric-card becomes a subgrid spanning them. Every card’s label, value, change, and link sit on the same row tracks, so links align across the row even when titles wrap to different lengths:
@layer components {
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
grid-template-rows: auto auto auto 1fr; /* last row pushes link to bottom */
gap: 1rem;
}
.metric-card {
display: grid;
grid-template-rows: subgrid; /* inherit the parent's 4 row tracks */
grid-row: span 4; /* span all four tracks */
}
}
Step 3 — container queries: reflow when the column is narrow. Each .metric-card declares itself a query container, and a wide-column card gets a horizontal layout while a narrow one stays stacked:
@layer components {
.metric-card {
container-type: inline-size;
}
@container (width >= 480px) {
.metric-card {
grid-template-rows: subgrid;
grid-template-columns: 1fr auto; /* value right, text left */
}
.metric-label,
.metric-change {
grid-column: 1;
}
.metric-value {
grid-column: 2;
grid-row: 1 / 4;
}
.metric-link {
grid-column: 1 / -1;
}
}
}
Now the same component adapts to wherever it’s placed — a 300px sidebar column gets the stacked layout, a 600px main column gets the two-column layout — with zero media queries and zero component knowledge of the viewport.
Fallback note: for browsers that predate container queries, lay out the card with a plain nested grid and a viewport @media as a progressive-enhancement baseline; the @container block simply doesn’t apply where unsupported.
Browser support in 2026
All three features are stable, baseline cross-browser, and safe for production:
| Feature | Support |
|---|---|
subgrid | Widely available across browsers since September 2023 |
Container size queries (@container, container-type, cq* units) | Widely available since 2023 |
@layer cascade layers | Widely available since March 2022 |
The newer additions — style queries, scroll-state queries, and container-type: scroll-state — are still graduating across engines, so pair them with the size-query core and feature-detect where you need them.
Putting It All Together
Here is the complete, runnable example — a .metrics grid of aligned cards that reflow by container width, all organized into cascade layers. Drop it into any current browser and open DevTools → Grid to see the subgrid tracks shared across cards:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Metric cards — subgrid, container queries, @layer</title>
<style>
@layer reset, base, components, utilities;
@layer reset {
* { box-sizing: border-box; margin: 0; }
body { padding: 2rem; font-family: system-ui, sans-serif; }
}
@layer components {
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
grid-template-rows: auto auto auto 1fr;
gap: 1rem;
}
.metric-card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 4;
gap: 8px;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 12px;
container-type: inline-size;
}
.metric-value {
font-size: 1.75rem;
font-weight: 700;
}
@container (width >= 480px) {
.metric-card {
grid-template-columns: 1fr auto;
}
.metric-label, .metric-change { grid-column: 1; }
.metric-value { grid-column: 2; grid-row: 1 / 4; }
.metric-link { grid-column: 1 / -1; }
}
}
</style>
</head>
<body>
<div class="metrics">
<article class="metric-card">
<h2 class="metric-label">Monthly revenue</h2>
<p class="metric-value">$42,580</p>
<p class="metric-change">+12.4% vs last month</p>
<a class="metric-link" href="/revenue">View report</a>
</article>
<article class="metric-card">
<h2 class="metric-label">Active users</h2>
<p class="metric-value">8,912</p>
<p class="metric-change">+3.1% vs last month</p>
<a class="metric-link" href="/users">View report</a>
</article>
</div>
</body>
</html>
Expected output: two equal-height cards whose labels, values, changes, and links all sit on shared row tracks (resize the browser and they stay aligned because of subgrid). When a card’s container is at least 480px wide, it switches to a two-column layout with the value on the right — and if you drop the same cards into a narrow sidebar container, they automatically fall back to the stacked layout.
Conclusion & Next Steps
You now have the three pillars of modern, maintainable CSS in 2026: subgrid ties nested grids to their parent’s tracks for perfect alignment, container queries let components respond to their own size with @container and cq* units, and cascade layers give you deterministic cascade order with @layer instead of specificity fights.
Next steps: refactor an existing card grid to use subgrid and delete your min-height hacks; convert a dashboard widget from viewport @media to container queries and reuse it in sidebar and main layouts; and introduce @layer reset, base, components, utilities; into a project that still relies on !important. Finally, experiment with the newer capabilities — style queries for theming, scroll-state queries for sticky navs, and container-type: scroll-state — as they finish rolling out.
References / Sources
- MDN — CSS: cascading style sheets reference. https://developer.mozilla.org/en-US/docs/Web/CSS
- MDN — CSS container queries. https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries
- MDN — Subgrid (CSS Grid Layout Module Level 2). https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Subgrid
- MDN —
@layerCSS at-rule (CSS Cascading and Inheritance Level 5). https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@layer - MDN —
@containerat-rule. https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@container - MDN — container query length units. https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length#container_query_length_units