16 / 20
All 20 in 20 CSS Container Queries
- 01Container Queries Getting Started
- 02Container Query Units cqi cqw cqmin
- 03Named Containers with container-name
- 04Card Component Adapting to Container Width
- 05Sidebar Aware Main Content Reflow
- 06Container Queries vs Media Queries
- 07CSS Style Queries with container style
- 08Scroll State Container Queries
- 09Fluid Typography with Container Units
- 10Dashboard Widget Component Resizing
- 11Responsive Product Card Grid Reflow
- 12Media Object Layout Inline to Stacked
- 13Dynamic Form Layout Adaptation
- 14Navigation Component Collapse by Container
- 15Responsive Table to Card Layout Switch
- 16Progressive Enhancement and Fallback Strategy
- 17Nested Container Contexts
- 18Avoiding Infinite Layout Loops
- 19Combining Container Queries with CSS Grid
- 20Debugging Container Query Sizing Issues
Progressive Enhancement and Fallback Strategy
How to ship container queries so an engine that lacks them still gets a good layout: a flex-wrap baseline declared first, then @supports (container-type: inline-size) gating the enhanced path. Baseline since February 2023, so the realistic fallback target is old enterprise browsers — the baseline should be a design you would ship, not a broken one.
Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS
ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link
The code
<section class="cq-16" aria-labelledby="cq-16-h">
<fieldset class="cq-16__theme">
<legend class="cq-16__themelegend">Theme</legend>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-auto" checked>
<label class="cq-16__themeopt" for="cq-16-theme-auto">Auto</label>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-light">
<label class="cq-16__themeopt" for="cq-16-theme-light">Light</label>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-dark">
<label class="cq-16__themeopt" for="cq-16-theme-dark">Dark</label>
</fieldset>
<div class="cq-16__stage">
<header class="cq-16__head">
<h2 class="cq-16__h" id="cq-16-h">Baseline first, enhancement after</h2>
<p class="cq-16__lede">Both panels ship the same markup and the same baseline rules. Only one of them establishes a container.</p>
</header>
<div class="cq-16__row">
<section class="cq-16__panel" aria-label="Baseline path">
<p class="cq-16__badge cq-16__badge--base">Baseline path · no container</p>
<div class="cq-16__host cq-16__host--nocq">
<div class="cq-16__list">
<article class="cq-16__lead">
<h3>Fern grant round three</h3>
<p>Applications close on the last Friday of the month; decisions within four weeks.</p>
</article>
<ul class="cq-16__facts">
<li><b>42</b>awards made</li>
<li><b>£1.8m</b>committed</li>
<li><b>9</b>regions</li>
</ul>
</div>
</div>
<p class="cq-16__foot">Wraps at its own <code>flex-basis</code>. No per-placement decision, still a good layout.</p>
</section>
<section class="cq-16__panel" aria-label="Enhanced path">
<p class="cq-16__badge cq-16__badge--enh">Enhanced path · container established</p>
<div class="cq-16__host">
<div class="cq-16__list">
<article class="cq-16__lead">
<h3>Fern grant round three</h3>
<p>Applications close on the last Friday of the month; decisions within four weeks.</p>
</article>
<ul class="cq-16__facts">
<li><b>42</b>awards made</li>
<li><b>£1.8m</b>committed</li>
<li><b>9</b>regions</li>
</ul>
</div>
</div>
<p class="cq-16__foot">Inside <code>@supports (container-type: inline-size)</code>, the query splits lead and facts by ratio.</p>
</section>
</div>
<p class="cq-16__note">Container queries are Baseline since February 2023. The baseline path is for locked-down enterprise builds and old WebViews — one solid fallback, not a parallel design.</p>
</div>
</section><section class="cq-16" aria-labelledby="cq-16-h">
<fieldset class="cq-16__theme">
<legend class="cq-16__themelegend">Theme</legend>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-auto" checked>
<label class="cq-16__themeopt" for="cq-16-theme-auto">Auto</label>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-light">
<label class="cq-16__themeopt" for="cq-16-theme-light">Light</label>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-dark">
<label class="cq-16__themeopt" for="cq-16-theme-dark">Dark</label>
</fieldset>
<div class="cq-16__stage">
<header class="cq-16__head">
<h2 class="cq-16__h" id="cq-16-h">Baseline first, enhancement after</h2>
<p class="cq-16__lede">Both panels ship the same markup and the same baseline rules. Only one of them establishes a container.</p>
</header>
<div class="cq-16__row">
<section class="cq-16__panel" aria-label="Baseline path">
<p class="cq-16__badge cq-16__badge--base">Baseline path · no container</p>
<div class="cq-16__host cq-16__host--nocq">
<div class="cq-16__list">
<article class="cq-16__lead">
<h3>Fern grant round three</h3>
<p>Applications close on the last Friday of the month; decisions within four weeks.</p>
</article>
<ul class="cq-16__facts">
<li><b>42</b>awards made</li>
<li><b>£1.8m</b>committed</li>
<li><b>9</b>regions</li>
</ul>
</div>
</div>
<p class="cq-16__foot">Wraps at its own <code>flex-basis</code>. No per-placement decision, still a good layout.</p>
</section>
<section class="cq-16__panel" aria-label="Enhanced path">
<p class="cq-16__badge cq-16__badge--enh">Enhanced path · container established</p>
<div class="cq-16__host">
<div class="cq-16__list">
<article class="cq-16__lead">
<h3>Fern grant round three</h3>
<p>Applications close on the last Friday of the month; decisions within four weeks.</p>
</article>
<ul class="cq-16__facts">
<li><b>42</b>awards made</li>
<li><b>£1.8m</b>committed</li>
<li><b>9</b>regions</li>
</ul>
</div>
</div>
<p class="cq-16__foot">Inside <code>@supports (container-type: inline-size)</code>, the query splits lead and facts by ratio.</p>
</section>
</div>
<p class="cq-16__note">Container queries are Baseline since February 2023. The baseline path is for locked-down enterprise builds and old WebViews — one solid fallback, not a parallel design.</p>
</div>
</section>.cq-16,
.cq-16[data-theme="light"] {
--bg: #ffffff;
--surf: #f7f9fb;
--line: #b9c6d4;
--ink: #12202c;
--mut: #5b6b7a;
--acc: #21618c;
--base: #7b6a5a;
}
.cq-16[data-theme="dark"] {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
@media (prefers-color-scheme: dark) {
.cq-16:not([data-theme="light"]) {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
}
.cq-16 {
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
font-family: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
line-height: 1.45;
}
.cq-16 *,
.cq-16 *::before,
.cq-16 *::after {
box-sizing: border-box;
}
.cq-16__stage {
max-width: 72rem;
margin-inline: auto;
padding: clamp(1rem, 3vw, 2.5rem);
display: grid;
gap: 1.25rem;
}
.cq-16__h {
margin: 0;
font-size: clamp(1.2rem, 1rem + 1.4vw, 1.7rem);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.cq-16__lede {
margin: 0.4rem 0 0;
font-size: 0.85rem;
color: var(--mut);
max-width: 58ch;
}
.cq-16__row {
display: flex;
flex-wrap: wrap;
gap: 1.25rem;
}
.cq-16__panel {
flex: 1 1 22rem;
min-inline-size: 0;
display: grid;
gap: 0.6rem;
align-content: start;
}
.cq-16__badge {
margin: 0;
padding: 0.3rem 0.55rem;
border: 1px dashed var(--base);
color: var(--base);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
width: fit-content;
max-inline-size: 100%;
}
.cq-16__badge--enh {
border-color: var(--acc);
color: var(--acc);
border-style: solid;
}
.cq-16__host {
min-inline-size: 0;
padding: 0.9rem;
background: var(--surf);
border: 1px dashed var(--line);
}
.cq-16__list {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.cq-16__list > * {
flex: 1 1 13rem;
min-inline-size: 0;
}
.cq-16__lead h3 {
margin: 0 0 0.3rem;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.cq-16__lead p {
margin: 0;
font-size: 0.82rem;
color: var(--mut);
}
.cq-16__facts {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.35rem;
}
.cq-16__facts li {
display: flex;
align-items: baseline;
gap: 0.5rem;
font-size: 0.74rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--mut);
border-block-end: 1px dashed var(--line);
padding-block-end: 0.25rem;
}
.cq-16__facts b {
font-size: 1.05rem;
font-variant-numeric: tabular-nums;
color: var(--ink);
letter-spacing: 0;
}
.cq-16__foot {
margin: 0;
font-size: 0.74rem;
color: var(--mut);
}
.cq-16__foot code,
.cq-16__note code {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.9em;
}
.cq-16__note {
margin: 0;
padding-top: 0.85rem;
border-top: 1px solid var(--line);
font-size: 0.78rem;
color: var(--mut);
max-width: 76ch;
}
@supports (container-type: inline-size) {
.cq-16__host {
container: cq-16-host / inline-size;
}
.cq-16__host--nocq {
container-type: normal;
}
@container cq-16-host (min-width: 24em) {
.cq-16__list {
display: grid;
grid-template-columns: 2fr minmax(0, 1fr);
gap: 1.5rem;
}
.cq-16__lead h3 {
font-size: 1.25rem;
}
.cq-16__facts li {
border-block-end-style: solid;
}
}
}
/* Theme switch (added 2026-08-25). Pure CSS: three radios + :has(), zero
JavaScript. Auto inherits both the OS preference and the site's
[data-theme] stamp; Light and Dark pin the palette and beat both. The
pinned values are the demo's own [data-theme] declarations reused
verbatim, so a toggled demo can never drift from how the same demo
renders under the site theme. */
.cq-16 {
position: relative;
}
.cq-16__theme {
position: absolute;
inset-block-start: 0.875rem;
inset-inline-end: 0.875rem;
z-index: 9;
display: flex;
align-items: center;
gap: 0.125rem;
margin: 0;
padding: 0.1875rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 999px;
background: color-mix(in srgb, currentColor 8%, transparent);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
font-family: system-ui, sans-serif;
}
.cq-16__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-16__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-16__themeopt {
display: inline-flex;
align-items: center;
justify-content: center;
min-block-size: 2.25rem;
min-inline-size: 2.75rem;
padding: 0 0.6875rem;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: inherit;
opacity: 0.55;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.cq-16__themeopt:hover {
opacity: 0.85;
}
.cq-16__themeinput:checked + .cq-16__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.cq-16__themeinput:focus-visible + .cq-16__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.cq-16:has(#cq-16-theme-light:checked) {
--bg: #ffffff;
--surf: #f7f9fb;
--line: #b9c6d4;
--ink: #12202c;
--mut: #5b6b7a;
--acc: #21618c;
--base: #7b6a5a;
}
.cq-16:has(#cq-16-theme-dark:checked) {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}.cq-16,
.cq-16[data-theme="light"] {
--bg: #ffffff;
--surf: #f7f9fb;
--line: #b9c6d4;
--ink: #12202c;
--mut: #5b6b7a;
--acc: #21618c;
--base: #7b6a5a;
}
.cq-16[data-theme="dark"] {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
@media (prefers-color-scheme: dark) {
.cq-16:not([data-theme="light"]) {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
}
.cq-16 {
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
font-family: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
line-height: 1.45;
}
.cq-16 *,
.cq-16 *::before,
.cq-16 *::after {
box-sizing: border-box;
}
.cq-16__stage {
max-width: 72rem;
margin-inline: auto;
padding: clamp(1rem, 3vw, 2.5rem);
display: grid;
gap: 1.25rem;
}
.cq-16__h {
margin: 0;
font-size: clamp(1.2rem, 1rem + 1.4vw, 1.7rem);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.cq-16__lede {
margin: 0.4rem 0 0;
font-size: 0.85rem;
color: var(--mut);
max-width: 58ch;
}
.cq-16__row {
display: flex;
flex-wrap: wrap;
gap: 1.25rem;
}
.cq-16__panel {
flex: 1 1 22rem;
min-inline-size: 0;
display: grid;
gap: 0.6rem;
align-content: start;
}
.cq-16__badge {
margin: 0;
padding: 0.3rem 0.55rem;
border: 1px dashed var(--base);
color: var(--base);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
width: fit-content;
max-inline-size: 100%;
}
.cq-16__badge--enh {
border-color: var(--acc);
color: var(--acc);
border-style: solid;
}
.cq-16__host {
min-inline-size: 0;
padding: 0.9rem;
background: var(--surf);
border: 1px dashed var(--line);
}
.cq-16__list {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.cq-16__list > * {
flex: 1 1 13rem;
min-inline-size: 0;
}
.cq-16__lead h3 {
margin: 0 0 0.3rem;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.cq-16__lead p {
margin: 0;
font-size: 0.82rem;
color: var(--mut);
}
.cq-16__facts {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.35rem;
}
.cq-16__facts li {
display: flex;
align-items: baseline;
gap: 0.5rem;
font-size: 0.74rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--mut);
border-block-end: 1px dashed var(--line);
padding-block-end: 0.25rem;
}
.cq-16__facts b {
font-size: 1.05rem;
font-variant-numeric: tabular-nums;
color: var(--ink);
letter-spacing: 0;
}
.cq-16__foot {
margin: 0;
font-size: 0.74rem;
color: var(--mut);
}
.cq-16__foot code,
.cq-16__note code {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.9em;
}
.cq-16__note {
margin: 0;
padding-top: 0.85rem;
border-top: 1px solid var(--line);
font-size: 0.78rem;
color: var(--mut);
max-width: 76ch;
}
@supports (container-type: inline-size) {
.cq-16__host {
container: cq-16-host / inline-size;
}
.cq-16__host--nocq {
container-type: normal;
}
@container cq-16-host (min-width: 24em) {
.cq-16__list {
display: grid;
grid-template-columns: 2fr minmax(0, 1fr);
gap: 1.5rem;
}
.cq-16__lead h3 {
font-size: 1.25rem;
}
.cq-16__facts li {
border-block-end-style: solid;
}
}
}
/* Theme switch (added 2026-08-25). Pure CSS: three radios + :has(), zero
JavaScript. Auto inherits both the OS preference and the site's
[data-theme] stamp; Light and Dark pin the palette and beat both. The
pinned values are the demo's own [data-theme] declarations reused
verbatim, so a toggled demo can never drift from how the same demo
renders under the site theme. */
.cq-16 {
position: relative;
}
.cq-16__theme {
position: absolute;
inset-block-start: 0.875rem;
inset-inline-end: 0.875rem;
z-index: 9;
display: flex;
align-items: center;
gap: 0.125rem;
margin: 0;
padding: 0.1875rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 999px;
background: color-mix(in srgb, currentColor 8%, transparent);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
font-family: system-ui, sans-serif;
}
.cq-16__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-16__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-16__themeopt {
display: inline-flex;
align-items: center;
justify-content: center;
min-block-size: 2.25rem;
min-inline-size: 2.75rem;
padding: 0 0.6875rem;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: inherit;
opacity: 0.55;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.cq-16__themeopt:hover {
opacity: 0.85;
}
.cq-16__themeinput:checked + .cq-16__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.cq-16__themeinput:focus-visible + .cq-16__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.cq-16:has(#cq-16-theme-light:checked) {
--bg: #ffffff;
--surf: #f7f9fb;
--line: #b9c6d4;
--ink: #12202c;
--mut: #5b6b7a;
--acc: #21618c;
--base: #7b6a5a;
}
.cq-16:has(#cq-16-theme-dark:checked) {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}Here's a working CSS Container Querie 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: Progressive Enhancement and Fallback Strategy
Source: https://codefronts.com/layouts/css-container-queries/progressive-enhancement-and-fallback-strategy/
How to ship container queries so an engine that lacks them still gets a good layout: a flex-wrap baseline declared first, then @supports (container-type: inline-size) gating the enhanced path. Baseline since February 2023, so the realistic fallback target is old enterprise browsers — the baseline should be a design you would ship, not a broken one.
## HTML
```html
<section class="cq-16" aria-labelledby="cq-16-h">
<fieldset class="cq-16__theme">
<legend class="cq-16__themelegend">Theme</legend>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-auto" checked>
<label class="cq-16__themeopt" for="cq-16-theme-auto">Auto</label>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-light">
<label class="cq-16__themeopt" for="cq-16-theme-light">Light</label>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-dark">
<label class="cq-16__themeopt" for="cq-16-theme-dark">Dark</label>
</fieldset>
<div class="cq-16__stage">
<header class="cq-16__head">
<h2 class="cq-16__h" id="cq-16-h">Baseline first, enhancement after</h2>
<p class="cq-16__lede">Both panels ship the same markup and the same baseline rules. Only one of them establishes a container.</p>
</header>
<div class="cq-16__row">
<section class="cq-16__panel" aria-label="Baseline path">
<p class="cq-16__badge cq-16__badge--base">Baseline path · no container</p>
<div class="cq-16__host cq-16__host--nocq">
<div class="cq-16__list">
<article class="cq-16__lead">
<h3>Fern grant round three</h3>
<p>Applications close on the last Friday of the month; decisions within four weeks.</p>
</article>
<ul class="cq-16__facts">
<li><b>42</b>awards made</li>
<li><b>£1.8m</b>committed</li>
<li><b>9</b>regions</li>
</ul>
</div>
</div>
<p class="cq-16__foot">Wraps at its own <code>flex-basis</code>. No per-placement decision, still a good layout.</p>
</section>
<section class="cq-16__panel" aria-label="Enhanced path">
<p class="cq-16__badge cq-16__badge--enh">Enhanced path · container established</p>
<div class="cq-16__host">
<div class="cq-16__list">
<article class="cq-16__lead">
<h3>Fern grant round three</h3>
<p>Applications close on the last Friday of the month; decisions within four weeks.</p>
</article>
<ul class="cq-16__facts">
<li><b>42</b>awards made</li>
<li><b>£1.8m</b>committed</li>
<li><b>9</b>regions</li>
</ul>
</div>
</div>
<p class="cq-16__foot">Inside <code>@supports (container-type: inline-size)</code>, the query splits lead and facts by ratio.</p>
</section>
</div>
<p class="cq-16__note">Container queries are Baseline since February 2023. The baseline path is for locked-down enterprise builds and old WebViews — one solid fallback, not a parallel design.</p>
</div>
</section>
```
## CSS
```css
.cq-16,
.cq-16[data-theme="light"] {
--bg: #ffffff;
--surf: #f7f9fb;
--line: #b9c6d4;
--ink: #12202c;
--mut: #5b6b7a;
--acc: #21618c;
--base: #7b6a5a;
}
.cq-16[data-theme="dark"] {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
@media (prefers-color-scheme: dark) {
.cq-16:not([data-theme="light"]) {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
}
.cq-16 {
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
font-family: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
line-height: 1.45;
}
.cq-16 *,
.cq-16 *::before,
.cq-16 *::after {
box-sizing: border-box;
}
.cq-16__stage {
max-width: 72rem;
margin-inline: auto;
padding: clamp(1rem, 3vw, 2.5rem);
display: grid;
gap: 1.25rem;
}
.cq-16__h {
margin: 0;
font-size: clamp(1.2rem, 1rem + 1.4vw, 1.7rem);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.cq-16__lede {
margin: 0.4rem 0 0;
font-size: 0.85rem;
color: var(--mut);
max-width: 58ch;
}
.cq-16__row {
display: flex;
flex-wrap: wrap;
gap: 1.25rem;
}
.cq-16__panel {
flex: 1 1 22rem;
min-inline-size: 0;
display: grid;
gap: 0.6rem;
align-content: start;
}
.cq-16__badge {
margin: 0;
padding: 0.3rem 0.55rem;
border: 1px dashed var(--base);
color: var(--base);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
width: fit-content;
max-inline-size: 100%;
}
.cq-16__badge--enh {
border-color: var(--acc);
color: var(--acc);
border-style: solid;
}
.cq-16__host {
min-inline-size: 0;
padding: 0.9rem;
background: var(--surf);
border: 1px dashed var(--line);
}
.cq-16__list {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.cq-16__list > * {
flex: 1 1 13rem;
min-inline-size: 0;
}
.cq-16__lead h3 {
margin: 0 0 0.3rem;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.cq-16__lead p {
margin: 0;
font-size: 0.82rem;
color: var(--mut);
}
.cq-16__facts {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.35rem;
}
.cq-16__facts li {
display: flex;
align-items: baseline;
gap: 0.5rem;
font-size: 0.74rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--mut);
border-block-end: 1px dashed var(--line);
padding-block-end: 0.25rem;
}
.cq-16__facts b {
font-size: 1.05rem;
font-variant-numeric: tabular-nums;
color: var(--ink);
letter-spacing: 0;
}
.cq-16__foot {
margin: 0;
font-size: 0.74rem;
color: var(--mut);
}
.cq-16__foot code,
.cq-16__note code {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.9em;
}
.cq-16__note {
margin: 0;
padding-top: 0.85rem;
border-top: 1px solid var(--line);
font-size: 0.78rem;
color: var(--mut);
max-width: 76ch;
}
@supports (container-type: inline-size) {
.cq-16__host {
container: cq-16-host / inline-size;
}
.cq-16__host--nocq {
container-type: normal;
}
@container cq-16-host (min-width: 24em) {
.cq-16__list {
display: grid;
grid-template-columns: 2fr minmax(0, 1fr);
gap: 1.5rem;
}
.cq-16__lead h3 {
font-size: 1.25rem;
}
.cq-16__facts li {
border-block-end-style: solid;
}
}
}
/* Theme switch (added 2026-08-25). Pure CSS: three radios + :has(), zero
JavaScript. Auto inherits both the OS preference and the site's
[data-theme] stamp; Light and Dark pin the palette and beat both. The
pinned values are the demo's own [data-theme] declarations reused
verbatim, so a toggled demo can never drift from how the same demo
renders under the site theme. */
.cq-16 {
position: relative;
}
.cq-16__theme {
position: absolute;
inset-block-start: 0.875rem;
inset-inline-end: 0.875rem;
z-index: 9;
display: flex;
align-items: center;
gap: 0.125rem;
margin: 0;
padding: 0.1875rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 999px;
background: color-mix(in srgb, currentColor 8%, transparent);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
font-family: system-ui, sans-serif;
}
.cq-16__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-16__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-16__themeopt {
display: inline-flex;
align-items: center;
justify-content: center;
min-block-size: 2.25rem;
min-inline-size: 2.75rem;
padding: 0 0.6875rem;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: inherit;
opacity: 0.55;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.cq-16__themeopt:hover {
opacity: 0.85;
}
.cq-16__themeinput:checked + .cq-16__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.cq-16__themeinput:focus-visible + .cq-16__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.cq-16:has(#cq-16-theme-light:checked) {
--bg: #ffffff;
--surf: #f7f9fb;
--line: #b9c6d4;
--ink: #12202c;
--mut: #5b6b7a;
--acc: #21618c;
--base: #7b6a5a;
}
.cq-16:has(#cq-16-theme-dark:checked) {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
```Here's a working CSS Container Querie 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: Progressive Enhancement and Fallback Strategy
Source: https://codefronts.com/layouts/css-container-queries/progressive-enhancement-and-fallback-strategy/
How to ship container queries so an engine that lacks them still gets a good layout: a flex-wrap baseline declared first, then @supports (container-type: inline-size) gating the enhanced path. Baseline since February 2023, so the realistic fallback target is old enterprise browsers — the baseline should be a design you would ship, not a broken one.
## HTML
```html
<section class="cq-16" aria-labelledby="cq-16-h">
<fieldset class="cq-16__theme">
<legend class="cq-16__themelegend">Theme</legend>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-auto" checked>
<label class="cq-16__themeopt" for="cq-16-theme-auto">Auto</label>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-light">
<label class="cq-16__themeopt" for="cq-16-theme-light">Light</label>
<input class="cq-16__themeinput" type="radio" name="cq-16-theme" id="cq-16-theme-dark">
<label class="cq-16__themeopt" for="cq-16-theme-dark">Dark</label>
</fieldset>
<div class="cq-16__stage">
<header class="cq-16__head">
<h2 class="cq-16__h" id="cq-16-h">Baseline first, enhancement after</h2>
<p class="cq-16__lede">Both panels ship the same markup and the same baseline rules. Only one of them establishes a container.</p>
</header>
<div class="cq-16__row">
<section class="cq-16__panel" aria-label="Baseline path">
<p class="cq-16__badge cq-16__badge--base">Baseline path · no container</p>
<div class="cq-16__host cq-16__host--nocq">
<div class="cq-16__list">
<article class="cq-16__lead">
<h3>Fern grant round three</h3>
<p>Applications close on the last Friday of the month; decisions within four weeks.</p>
</article>
<ul class="cq-16__facts">
<li><b>42</b>awards made</li>
<li><b>£1.8m</b>committed</li>
<li><b>9</b>regions</li>
</ul>
</div>
</div>
<p class="cq-16__foot">Wraps at its own <code>flex-basis</code>. No per-placement decision, still a good layout.</p>
</section>
<section class="cq-16__panel" aria-label="Enhanced path">
<p class="cq-16__badge cq-16__badge--enh">Enhanced path · container established</p>
<div class="cq-16__host">
<div class="cq-16__list">
<article class="cq-16__lead">
<h3>Fern grant round three</h3>
<p>Applications close on the last Friday of the month; decisions within four weeks.</p>
</article>
<ul class="cq-16__facts">
<li><b>42</b>awards made</li>
<li><b>£1.8m</b>committed</li>
<li><b>9</b>regions</li>
</ul>
</div>
</div>
<p class="cq-16__foot">Inside <code>@supports (container-type: inline-size)</code>, the query splits lead and facts by ratio.</p>
</section>
</div>
<p class="cq-16__note">Container queries are Baseline since February 2023. The baseline path is for locked-down enterprise builds and old WebViews — one solid fallback, not a parallel design.</p>
</div>
</section>
```
## CSS
```css
.cq-16,
.cq-16[data-theme="light"] {
--bg: #ffffff;
--surf: #f7f9fb;
--line: #b9c6d4;
--ink: #12202c;
--mut: #5b6b7a;
--acc: #21618c;
--base: #7b6a5a;
}
.cq-16[data-theme="dark"] {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
@media (prefers-color-scheme: dark) {
.cq-16:not([data-theme="light"]) {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
}
.cq-16 {
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
font-family: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
line-height: 1.45;
}
.cq-16 *,
.cq-16 *::before,
.cq-16 *::after {
box-sizing: border-box;
}
.cq-16__stage {
max-width: 72rem;
margin-inline: auto;
padding: clamp(1rem, 3vw, 2.5rem);
display: grid;
gap: 1.25rem;
}
.cq-16__h {
margin: 0;
font-size: clamp(1.2rem, 1rem + 1.4vw, 1.7rem);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.cq-16__lede {
margin: 0.4rem 0 0;
font-size: 0.85rem;
color: var(--mut);
max-width: 58ch;
}
.cq-16__row {
display: flex;
flex-wrap: wrap;
gap: 1.25rem;
}
.cq-16__panel {
flex: 1 1 22rem;
min-inline-size: 0;
display: grid;
gap: 0.6rem;
align-content: start;
}
.cq-16__badge {
margin: 0;
padding: 0.3rem 0.55rem;
border: 1px dashed var(--base);
color: var(--base);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
width: fit-content;
max-inline-size: 100%;
}
.cq-16__badge--enh {
border-color: var(--acc);
color: var(--acc);
border-style: solid;
}
.cq-16__host {
min-inline-size: 0;
padding: 0.9rem;
background: var(--surf);
border: 1px dashed var(--line);
}
.cq-16__list {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.cq-16__list > * {
flex: 1 1 13rem;
min-inline-size: 0;
}
.cq-16__lead h3 {
margin: 0 0 0.3rem;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.cq-16__lead p {
margin: 0;
font-size: 0.82rem;
color: var(--mut);
}
.cq-16__facts {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.35rem;
}
.cq-16__facts li {
display: flex;
align-items: baseline;
gap: 0.5rem;
font-size: 0.74rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--mut);
border-block-end: 1px dashed var(--line);
padding-block-end: 0.25rem;
}
.cq-16__facts b {
font-size: 1.05rem;
font-variant-numeric: tabular-nums;
color: var(--ink);
letter-spacing: 0;
}
.cq-16__foot {
margin: 0;
font-size: 0.74rem;
color: var(--mut);
}
.cq-16__foot code,
.cq-16__note code {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.9em;
}
.cq-16__note {
margin: 0;
padding-top: 0.85rem;
border-top: 1px solid var(--line);
font-size: 0.78rem;
color: var(--mut);
max-width: 76ch;
}
@supports (container-type: inline-size) {
.cq-16__host {
container: cq-16-host / inline-size;
}
.cq-16__host--nocq {
container-type: normal;
}
@container cq-16-host (min-width: 24em) {
.cq-16__list {
display: grid;
grid-template-columns: 2fr minmax(0, 1fr);
gap: 1.5rem;
}
.cq-16__lead h3 {
font-size: 1.25rem;
}
.cq-16__facts li {
border-block-end-style: solid;
}
}
}
/* Theme switch (added 2026-08-25). Pure CSS: three radios + :has(), zero
JavaScript. Auto inherits both the OS preference and the site's
[data-theme] stamp; Light and Dark pin the palette and beat both. The
pinned values are the demo's own [data-theme] declarations reused
verbatim, so a toggled demo can never drift from how the same demo
renders under the site theme. */
.cq-16 {
position: relative;
}
.cq-16__theme {
position: absolute;
inset-block-start: 0.875rem;
inset-inline-end: 0.875rem;
z-index: 9;
display: flex;
align-items: center;
gap: 0.125rem;
margin: 0;
padding: 0.1875rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 999px;
background: color-mix(in srgb, currentColor 8%, transparent);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
font-family: system-ui, sans-serif;
}
.cq-16__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-16__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-16__themeopt {
display: inline-flex;
align-items: center;
justify-content: center;
min-block-size: 2.25rem;
min-inline-size: 2.75rem;
padding: 0 0.6875rem;
border-radius: 999px;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: inherit;
opacity: 0.55;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.cq-16__themeopt:hover {
opacity: 0.85;
}
.cq-16__themeinput:checked + .cq-16__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.cq-16__themeinput:focus-visible + .cq-16__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.cq-16:has(#cq-16-theme-light:checked) {
--bg: #ffffff;
--surf: #f7f9fb;
--line: #b9c6d4;
--ink: #12202c;
--mut: #5b6b7a;
--acc: #21618c;
--base: #7b6a5a;
}
.cq-16:has(#cq-16-theme-dark:checked) {
--bg: #0b1218;
--surf: #111c25;
--line: #2a3d4d;
--ink: #e7eff5;
--mut: #93a5b4;
--acc: #6fb2e0;
--base: #c0aa93;
}
```How this works
Write the baseline as a layout you would ship. flex-wrap with a sensible flex-basis gets you a responsive component with no queries at all — it just cannot make decisions per placement. That is the floor, and it goes first:
.list { display: flex; flex-wrap: wrap; gap: 1rem; }
.list > * { flex: 1 1 14rem; } /* baseline: good, not broken */
@supports (container-type: inline-size) {
.host { container: cq-16-host / inline-size; }
@container cq-16-host (min-width: 24em) { .list { display: grid; grid-template-columns: 1fr 2fr; } }
}The gate belongs on the container, not on the query. An unsupported engine already ignores @container blocks, so the reason to wrap things in @supports is to avoid applying the containment itself — and the side effects that come with it — where the query cannot pay for it. Keep the enhanced rules inside the same gate so the whole enhancement is one block you can read at a glance.
Be accurate about who the fallback is for. Container queries reached Baseline in February 2023 (Chrome 105, Safari 16, Firefox 110). The people who need the baseline path are on locked-down enterprise builds, old embedded WebViews and long-tail Android browsers — not the general web. That should change how much you invest: one good flex baseline, not a parallel design.
The trap: enhancing content instead of layout. If the enhanced path reveals information the baseline hides, unsupported users lose content rather than polish. Keep everything in the DOM in both paths and let the query decide arrangement only — then the fallback is a plainer version of the same page rather than a lesser one.
Make it yours
- Change the baseline
flex-basisto tune how the unsupported path wraps; that single value is the whole fallback. - Add a second enhanced stage inside the same
@supportsblock — the gate covers everything nested in it. - Gate a different feature the same way:
@supports (container-type: scroll-state)for demo 08's states, or a fallback modifier class for demo 07's style queries. - Invert the demo to check your own component: delete the
container-typedeclaration and see whether what is left is still shippable. - Swap the dashed chrome for solid borders if this sits next to softer components.
- Retint the two path badges from
--accand--mut.
Gotchas — read before shipping
- Testing
@supports (container-type: inline-size)tells you the property parses, not that@containerqueries evaluate — in practice they shipped together, but do not treat the test as a guarantee for newer sub-features likestyle()orscroll-state. - Putting content inside the enhanced path means unsupported engines lose information, not just layout. Enhance arrangement only.
- A baseline written as
display: gridwith fixed columns can be worse than no layout at all on a narrow screen; preferflex-wraporauto-fitfor the floor. - Applying
container-typeoutside the gate still applies containment in engines that parse it but do not evaluate queries, which can change positioning for no benefit.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.2+ | 121+ | 111+ |
Floor set by :has(), color-mix(), backdrop-filter, @container as this demo is written. The core technique itself goes back further — Chrome 105+.
This demo is about the gate itself, so it has two floors. The baseline flex-wrap path needs nothing beyond flexbox and works in any engine shipped in the last decade; the enhanced path needs container-type: inline-size and @container, which is Chrome 105 / Safari 16 / Firefox 110 and Baseline since February 2023. Nothing here uses oklch(), so the numbers above are the container query floor exactly. Below it, the component renders the wrapping flex layout with no containment applied at all.