30 CSS Grid Layouts20 / 30

Pure CSSMIT licensed

Container-Query Card

One card component, two layouts — decided by the card's OWN width, not the viewport. Dropped into the wide main column it goes horizontal; the identical markup in the narrow sidebar stacks vertically. This is the component-portability upgrade media queries could never deliver.

Published Updated

Live Demo
Try it

The code

<section class="cgl-20" aria-label="Container query card demo">
  <div class="cgl-20__page">
    <main class="cgl-20__main">
      <p class="cgl-20__where">main · wide container → horizontal card</p>
      <div class="cgl-20__holder">
        <article class="cgl-20__card">
          <div class="cgl-20__media" role="img" aria-label="Coastal trail placeholder"></div>
          <div class="cgl-20__body"><h4>Coastal trail guide</h4><p>Same markup, same stylesheet. The card asked its container — not the viewport — how much room it has.</p><a href="#0">Read guide →</a></div>
        </article>
      </div>
    </main>
    <aside class="cgl-20__aside">
      <p class="cgl-20__where">aside · narrow → stacked</p>
      <div class="cgl-20__holder">
        <article class="cgl-20__card">
          <div class="cgl-20__media" role="img" aria-label="Coastal trail placeholder"></div>
          <div class="cgl-20__body"><h4>Coastal trail guide</h4><p>Identical component, adapted by @container.</p><a href="#0">Read guide →</a></div>
        </article>
      </div>
    </aside>
  </div>
</section>
.cgl-20,
.cgl-20 *,
.cgl-20 *::before,
.cgl-20 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cgl-20 {
  width: 100%;
  min-height: 100vh;
  --peach: oklch(0.78 0.11 45);
  --ink: oklch(0.28 0.04 45);
  --bg: oklch(0.965 0.012 60);
  font-family: system-ui,sans-serif;
  background: var(--bg);
  padding: 1.4rem;
}

.cgl-20 .cgl-20__page {
  display: grid;
  grid-template-columns: minmax(0,2fr) minmax(0,1fr);
  gap: 14px;
}

.cgl-20 .cgl-20__where {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .66rem;
  color: color-mix(in oklab,var(--ink) 60%,var(--bg));
  margin-block-end: .5rem;
}

.cgl-20 .cgl-20__holder {
  container-type: inline-size;
  container-name: card;
}

.cgl-20 .cgl-20__card {
  display: grid;
  grid-template-areas: "media" "body";
  border-radius: 14px;
  overflow: hidden;
  background: white;
  border: 1px solid color-mix(in oklab,var(--peach) 45%,white);
  box-shadow: 0 12px 28px -20px oklch(0.45 0.08 45/.7);
}

.cgl-20 .cgl-20__media {
  grid-area: media;
  min-block-size: 110px;
  background: linear-gradient(140deg,var(--peach),oklch(0.55 0.12 25));
}

.cgl-20 .cgl-20__body {
  grid-area: body;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: .5rem;
}

.cgl-20 h4 {
  font-size: clamp(.9rem,4.5cqi,1.15rem);
  color: var(--ink);
}

.cgl-20 .cgl-20__body p {
  font-size: .78rem;
  line-height: 1.55;
  color: color-mix(in oklab,var(--ink) 70%,white);
}

.cgl-20 .cgl-20__body a {
  font-size: .78rem;
  font-weight: 600;
  color: oklch(0.55 0.14 35);
  text-decoration: none;
  margin-block-start: auto;
}

.cgl-20 .cgl-20__body a:hover {
  text-decoration: underline;
}

.cgl-20 .cgl-20__body a:focus-visible {
  outline: 2px solid var(--peach);
  outline-offset: 2px;
}

@container card (min-width: 380px) {
  .cgl-20 .cgl-20__card {
    grid-template-areas: "media body";
    grid-template-columns: 140px minmax(0,1fr);
  }

  .cgl-20 .cgl-20__media {
    min-block-size: 100%;
  }
}

@media (max-width: 600px) {
  .cgl-20 .cgl-20__page {
    grid-template-columns: minmax(0,1fr);
  }
}
/* No JavaScript — container-type: inline-size on the wrapper; @container swaps the card's internal grid-template-areas. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Grid Layout from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Container-Query Card
Source: https://codefronts.com/layouts/css-grid-layouts/container-query-card/

One card component, two layouts — decided by the card's OWN width, not the viewport. Dropped into the wide main column it goes horizontal; the identical markup in the narrow sidebar stacks vertically. This is the component-portability upgrade media queries could never deliver.
## HTML
```html
<section class="cgl-20" aria-label="Container query card demo">
  <div class="cgl-20__page">
    <main class="cgl-20__main">
      <p class="cgl-20__where">main · wide container → horizontal card</p>
      <div class="cgl-20__holder">
        <article class="cgl-20__card">
          <div class="cgl-20__media" role="img" aria-label="Coastal trail placeholder"></div>
          <div class="cgl-20__body"><h4>Coastal trail guide</h4><p>Same markup, same stylesheet. The card asked its container — not the viewport — how much room it has.</p><a href="#0">Read guide →</a></div>
        </article>
      </div>
    </main>
    <aside class="cgl-20__aside">
      <p class="cgl-20__where">aside · narrow → stacked</p>
      <div class="cgl-20__holder">
        <article class="cgl-20__card">
          <div class="cgl-20__media" role="img" aria-label="Coastal trail placeholder"></div>
          <div class="cgl-20__body"><h4>Coastal trail guide</h4><p>Identical component, adapted by @container.</p><a href="#0">Read guide →</a></div>
        </article>
      </div>
    </aside>
  </div>
</section>
```
## CSS
```css
.cgl-20,
.cgl-20 *,
.cgl-20 *::before,
.cgl-20 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cgl-20 {
  width: 100%;
  min-height: 100vh;
  --peach: oklch(0.78 0.11 45);
  --ink: oklch(0.28 0.04 45);
  --bg: oklch(0.965 0.012 60);
  font-family: system-ui,sans-serif;
  background: var(--bg);
  padding: 1.4rem;
}

.cgl-20 .cgl-20__page {
  display: grid;
  grid-template-columns: minmax(0,2fr) minmax(0,1fr);
  gap: 14px;
}

.cgl-20 .cgl-20__where {
  font-family: ui-monospace,Menlo,monospace;
  font-size: .66rem;
  color: color-mix(in oklab,var(--ink) 60%,var(--bg));
  margin-block-end: .5rem;
}

.cgl-20 .cgl-20__holder {
  container-type: inline-size;
  container-name: card;
}

.cgl-20 .cgl-20__card {
  display: grid;
  grid-template-areas: "media" "body";
  border-radius: 14px;
  overflow: hidden;
  background: white;
  border: 1px solid color-mix(in oklab,var(--peach) 45%,white);
  box-shadow: 0 12px 28px -20px oklch(0.45 0.08 45/.7);
}

.cgl-20 .cgl-20__media {
  grid-area: media;
  min-block-size: 110px;
  background: linear-gradient(140deg,var(--peach),oklch(0.55 0.12 25));
}

.cgl-20 .cgl-20__body {
  grid-area: body;
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: .5rem;
}

.cgl-20 h4 {
  font-size: clamp(.9rem,4.5cqi,1.15rem);
  color: var(--ink);
}

.cgl-20 .cgl-20__body p {
  font-size: .78rem;
  line-height: 1.55;
  color: color-mix(in oklab,var(--ink) 70%,white);
}

.cgl-20 .cgl-20__body a {
  font-size: .78rem;
  font-weight: 600;
  color: oklch(0.55 0.14 35);
  text-decoration: none;
  margin-block-start: auto;
}

.cgl-20 .cgl-20__body a:hover {
  text-decoration: underline;
}

.cgl-20 .cgl-20__body a:focus-visible {
  outline: 2px solid var(--peach);
  outline-offset: 2px;
}

@container card (min-width: 380px) {
  .cgl-20 .cgl-20__card {
    grid-template-areas: "media body";
    grid-template-columns: 140px minmax(0,1fr);
  }

  .cgl-20 .cgl-20__media {
    min-block-size: 100%;
  }
}

@media (max-width: 600px) {
  .cgl-20 .cgl-20__page {
    grid-template-columns: minmax(0,1fr);
  }
}
```

## JavaScript
```js
/* No JavaScript — container-type: inline-size on the wrapper; @container swaps the card's internal grid-template-areas. */
```

How this works

The card's wrapper declares container-type: inline-size; container-name: card. Inside, @container card (min-width: 380px) switches the card's internal grid from stacked (grid-template-areas: "media" "body") to horizontal ("media body" with a 140px media column). The query responds to the CONTAINER's width — the same component adapts to wherever it's dropped, with zero knowledge of page layout.

The demo places identical markup in a 2fr main region and a 1fr aside to prove it: same class, same CSS, different rendered layout. Container query units (cqi) scale the title typography with the card itself — component-relative typography, impossible pre-2023.

Make it yours

  • Add a third tier (@container (min-width: 560px)) for a full-bleed feature variant.
  • Name containers uniquely when nesting cards inside card lists to avoid query capture.
  • cqi type scaling: clamp(0.9rem, 4cqi, 1.15rem) tracks the card, not the screen.
  • This composes with the RAM grid — cards in an auto-fit grid self-adapt as tracks resize.

Gotchas — read before shipping

  • A container can't size itself from its contents on the queried axis — inline-size containment means the card's width must come from its context (grid track, flex basis).
  • Query the WRAPPER, style the CHILD — an element can't @container-query itself.
  • Unnamed nested containers capture descendant queries; name them (container-name) in any non-trivial tree.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

Floor set by oklch(), color-mix(), @container as this demo is written. The core technique itself goes back further — Chrome 105+.

Size container queries are Baseline 2023 — safe for all current evergreen audiences; no fallback needed beyond the stacked default.

Techniques used in this demo

Search CodeFronts

Loading…