09 / 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
Fluid Typography with Container Units
Type that scales with its container instead of the window: clamp(1.05rem, 4cqi, 2.1rem) beside clamp(1.05rem, 4vw, 2.1rem) in two identical narrow cards, so the difference is visible rather than asserted. The vw version is sized by a window it knows nothing about; the cqi version is sized by the card it actually lives in.
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-09" aria-labelledby="cq-09-h">
<fieldset class="cq-09__theme">
<legend class="cq-09__themelegend">Theme</legend>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-auto" checked>
<label class="cq-09__themeopt" for="cq-09-theme-auto">Auto</label>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-light">
<label class="cq-09__themeopt" for="cq-09-theme-light">Light</label>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-dark">
<label class="cq-09__themeopt" for="cq-09-theme-dark">Dark</label>
</fieldset>
<div class="cq-09__stage">
<header class="cq-09__head">
<p class="cq-09__kicker">Type & measure</p>
<h2 class="cq-09__h" id="cq-09-h">Sized by the card, not the window</h2>
<p class="cq-09__lede">Two identical cards, one declaration apart. Widen the browser and watch only one of them lose its proportions.</p>
</header>
<div class="cq-09__row">
<figure class="cq-09__fig">
<div class="cq-09__card">
<p class="cq-09__eyebrow">Essay</p>
<h3 class="cq-09__title cq-09__title--cq">The quiet return of the small page</h3>
<p class="cq-09__body">Reading room design went wide for a decade. The interesting work went back to the column.</p>
</div>
<figcaption><code>clamp(1.05rem, 4cqi, 2.1rem)</code> — reads the card</figcaption>
</figure>
<figure class="cq-09__fig">
<div class="cq-09__card">
<p class="cq-09__eyebrow">Essay</p>
<h3 class="cq-09__title cq-09__title--vw">The quiet return of the small page</h3>
<p class="cq-09__body">Reading room design went wide for a decade. The interesting work went back to the column.</p>
</div>
<figcaption class="cq-09__bad"><code>clamp(1.05rem, 4vw, 2.1rem)</code> — reads the window</figcaption>
</figure>
</div>
<figure class="cq-09__fig cq-09__fig--wide">
<div class="cq-09__card cq-09__card--resize">
<p class="cq-09__eyebrow">Drag the right edge</p>
<h3 class="cq-09__title cq-09__title--cq">Every heading here is one declaration</h3>
<p class="cq-09__body">The container changes width, the type follows, and the paragraph stays in rem so text settings still apply.</p>
</div>
<figcaption>Same rule, driven by hand with <code>resize: horizontal</code></figcaption>
</figure>
</div>
</section><section class="cq-09" aria-labelledby="cq-09-h">
<fieldset class="cq-09__theme">
<legend class="cq-09__themelegend">Theme</legend>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-auto" checked>
<label class="cq-09__themeopt" for="cq-09-theme-auto">Auto</label>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-light">
<label class="cq-09__themeopt" for="cq-09-theme-light">Light</label>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-dark">
<label class="cq-09__themeopt" for="cq-09-theme-dark">Dark</label>
</fieldset>
<div class="cq-09__stage">
<header class="cq-09__head">
<p class="cq-09__kicker">Type & measure</p>
<h2 class="cq-09__h" id="cq-09-h">Sized by the card, not the window</h2>
<p class="cq-09__lede">Two identical cards, one declaration apart. Widen the browser and watch only one of them lose its proportions.</p>
</header>
<div class="cq-09__row">
<figure class="cq-09__fig">
<div class="cq-09__card">
<p class="cq-09__eyebrow">Essay</p>
<h3 class="cq-09__title cq-09__title--cq">The quiet return of the small page</h3>
<p class="cq-09__body">Reading room design went wide for a decade. The interesting work went back to the column.</p>
</div>
<figcaption><code>clamp(1.05rem, 4cqi, 2.1rem)</code> — reads the card</figcaption>
</figure>
<figure class="cq-09__fig">
<div class="cq-09__card">
<p class="cq-09__eyebrow">Essay</p>
<h3 class="cq-09__title cq-09__title--vw">The quiet return of the small page</h3>
<p class="cq-09__body">Reading room design went wide for a decade. The interesting work went back to the column.</p>
</div>
<figcaption class="cq-09__bad"><code>clamp(1.05rem, 4vw, 2.1rem)</code> — reads the window</figcaption>
</figure>
</div>
<figure class="cq-09__fig cq-09__fig--wide">
<div class="cq-09__card cq-09__card--resize">
<p class="cq-09__eyebrow">Drag the right edge</p>
<h3 class="cq-09__title cq-09__title--cq">Every heading here is one declaration</h3>
<p class="cq-09__body">The container changes width, the type follows, and the paragraph stays in rem so text settings still apply.</p>
</div>
<figcaption>Same rule, driven by hand with <code>resize: horizontal</code></figcaption>
</figure>
</div>
</section>.cq-09,
.cq-09[data-theme="light"] {
--bg: #fbfaf7;
--ink: #14120e;
--mut: #635c50;
--line: #ddd6c8;
--acc: #a86a10;
--bad: #a3341f;
}
.cq-09[data-theme="dark"] {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
@media (prefers-color-scheme: dark) {
.cq-09:not([data-theme="light"]) {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
}
@supports (color: oklch(0.5 0.1 250)) {
.cq-09,
.cq-09[data-theme="light"] {
--acc: oklch(0.58 0.12 68);
--bad: oklch(0.5 0.16 30);
}
.cq-09[data-theme="dark"] {
--acc: oklch(0.82 0.13 78);
--bad: oklch(0.72 0.14 32);
}
@media (prefers-color-scheme: dark) {
.cq-09:not([data-theme="light"]) {
--acc: oklch(0.82 0.13 78);
--bad: oklch(0.72 0.14 32);
}
}
}
.cq-09 {
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
line-height: 1.6;
}
.cq-09 *,
.cq-09 *::before,
.cq-09 *::after {
box-sizing: border-box;
}
.cq-09__stage {
max-width: 62rem;
margin-inline: auto;
padding: clamp(1.25rem, 4vw, 3rem);
display: grid;
gap: 2rem;
}
.cq-09__kicker {
margin: 0 0 0.5rem;
font-family: system-ui, sans-serif;
font-size: 0.7rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--acc);
}
.cq-09__h {
margin: 0;
font-size: clamp(1.6rem, 1.2rem + 2.4vw, 2.6rem);
font-weight: 400;
line-height: 1.15;
letter-spacing: -0.015em;
text-wrap: balance;
}
.cq-09__lede {
margin: 0.6rem 0 0;
color: var(--mut);
font-size: 1rem;
max-width: 46ch;
}
.cq-09__row {
display: flex;
flex-wrap: wrap;
gap: 2rem;
}
.cq-09__fig {
margin: 0;
flex: 0 1 17rem;
min-inline-size: 0;
display: grid;
gap: 0.6rem;
}
.cq-09__fig--wide {
flex: 1 1 100%;
}
.cq-09__fig figcaption {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.7rem;
color: var(--mut);
padding-top: 0.5rem;
border-top: 1px solid var(--line);
overflow-wrap: anywhere;
}
.cq-09__bad {
color: var(--bad);
}
.cq-09__card {
container: cq-09-card / inline-size;
padding: 1.25rem;
border-inline-start: 2px solid var(--line);
}
.cq-09__card--resize {
resize: horizontal;
overflow: auto;
min-inline-size: 12rem;
max-inline-size: 100%;
border: 1px dashed var(--line);
border-inline-start-width: 2px;
border-inline-start-style: solid;
}
.cq-09__eyebrow {
margin: 0 0 0.35rem;
font-family: system-ui, sans-serif;
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--acc);
}
.cq-09__title {
margin: 0 0 0.5rem;
font-weight: 400;
line-height: 1.12;
letter-spacing: -0.01em;
text-wrap: balance;
}
.cq-09__title--cq {
font-size: 1.35rem;
font-size: clamp(1.05rem, 4cqi, 2.1rem);
}
.cq-09__title--vw {
font-size: 1.35rem;
font-size: clamp(1.05rem, 4vw, 2.1rem);
}
.cq-09__body {
margin: 0;
font-size: 0.95rem;
color: var(--mut);
}
/* 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-09 {
position: relative;
}
.cq-09__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-09__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-09__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-09__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-09__themeopt:hover {
opacity: 0.85;
}
.cq-09__themeinput:checked + .cq-09__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.cq-09__themeinput:focus-visible + .cq-09__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.cq-09:has(#cq-09-theme-light:checked) {
--bg: #fbfaf7;
--ink: #14120e;
--mut: #635c50;
--line: #ddd6c8;
--acc: #a86a10;
--bad: #a3341f;
}
.cq-09:has(#cq-09-theme-dark:checked) {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}.cq-09,
.cq-09[data-theme="light"] {
--bg: #fbfaf7;
--ink: #14120e;
--mut: #635c50;
--line: #ddd6c8;
--acc: #a86a10;
--bad: #a3341f;
}
.cq-09[data-theme="dark"] {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
@media (prefers-color-scheme: dark) {
.cq-09:not([data-theme="light"]) {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
}
@supports (color: oklch(0.5 0.1 250)) {
.cq-09,
.cq-09[data-theme="light"] {
--acc: oklch(0.58 0.12 68);
--bad: oklch(0.5 0.16 30);
}
.cq-09[data-theme="dark"] {
--acc: oklch(0.82 0.13 78);
--bad: oklch(0.72 0.14 32);
}
@media (prefers-color-scheme: dark) {
.cq-09:not([data-theme="light"]) {
--acc: oklch(0.82 0.13 78);
--bad: oklch(0.72 0.14 32);
}
}
}
.cq-09 {
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
line-height: 1.6;
}
.cq-09 *,
.cq-09 *::before,
.cq-09 *::after {
box-sizing: border-box;
}
.cq-09__stage {
max-width: 62rem;
margin-inline: auto;
padding: clamp(1.25rem, 4vw, 3rem);
display: grid;
gap: 2rem;
}
.cq-09__kicker {
margin: 0 0 0.5rem;
font-family: system-ui, sans-serif;
font-size: 0.7rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--acc);
}
.cq-09__h {
margin: 0;
font-size: clamp(1.6rem, 1.2rem + 2.4vw, 2.6rem);
font-weight: 400;
line-height: 1.15;
letter-spacing: -0.015em;
text-wrap: balance;
}
.cq-09__lede {
margin: 0.6rem 0 0;
color: var(--mut);
font-size: 1rem;
max-width: 46ch;
}
.cq-09__row {
display: flex;
flex-wrap: wrap;
gap: 2rem;
}
.cq-09__fig {
margin: 0;
flex: 0 1 17rem;
min-inline-size: 0;
display: grid;
gap: 0.6rem;
}
.cq-09__fig--wide {
flex: 1 1 100%;
}
.cq-09__fig figcaption {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.7rem;
color: var(--mut);
padding-top: 0.5rem;
border-top: 1px solid var(--line);
overflow-wrap: anywhere;
}
.cq-09__bad {
color: var(--bad);
}
.cq-09__card {
container: cq-09-card / inline-size;
padding: 1.25rem;
border-inline-start: 2px solid var(--line);
}
.cq-09__card--resize {
resize: horizontal;
overflow: auto;
min-inline-size: 12rem;
max-inline-size: 100%;
border: 1px dashed var(--line);
border-inline-start-width: 2px;
border-inline-start-style: solid;
}
.cq-09__eyebrow {
margin: 0 0 0.35rem;
font-family: system-ui, sans-serif;
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--acc);
}
.cq-09__title {
margin: 0 0 0.5rem;
font-weight: 400;
line-height: 1.12;
letter-spacing: -0.01em;
text-wrap: balance;
}
.cq-09__title--cq {
font-size: 1.35rem;
font-size: clamp(1.05rem, 4cqi, 2.1rem);
}
.cq-09__title--vw {
font-size: 1.35rem;
font-size: clamp(1.05rem, 4vw, 2.1rem);
}
.cq-09__body {
margin: 0;
font-size: 0.95rem;
color: var(--mut);
}
/* 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-09 {
position: relative;
}
.cq-09__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-09__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-09__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-09__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-09__themeopt:hover {
opacity: 0.85;
}
.cq-09__themeinput:checked + .cq-09__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.cq-09__themeinput:focus-visible + .cq-09__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.cq-09:has(#cq-09-theme-light:checked) {
--bg: #fbfaf7;
--ink: #14120e;
--mut: #635c50;
--line: #ddd6c8;
--acc: #a86a10;
--bad: #a3341f;
}
.cq-09:has(#cq-09-theme-dark:checked) {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}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: Fluid Typography with Container Units
Source: https://codefronts.com/layouts/css-container-queries/fluid-typography-with-container-units/
Type that scales with its container instead of the window: clamp(1.05rem, 4cqi, 2.1rem) beside clamp(1.05rem, 4vw, 2.1rem) in two identical narrow cards, so the difference is visible rather than asserted. The vw version is sized by a window it knows nothing about; the cqi version is sized by the card it actually lives in.
## HTML
```html
<section class="cq-09" aria-labelledby="cq-09-h">
<fieldset class="cq-09__theme">
<legend class="cq-09__themelegend">Theme</legend>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-auto" checked>
<label class="cq-09__themeopt" for="cq-09-theme-auto">Auto</label>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-light">
<label class="cq-09__themeopt" for="cq-09-theme-light">Light</label>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-dark">
<label class="cq-09__themeopt" for="cq-09-theme-dark">Dark</label>
</fieldset>
<div class="cq-09__stage">
<header class="cq-09__head">
<p class="cq-09__kicker">Type & measure</p>
<h2 class="cq-09__h" id="cq-09-h">Sized by the card, not the window</h2>
<p class="cq-09__lede">Two identical cards, one declaration apart. Widen the browser and watch only one of them lose its proportions.</p>
</header>
<div class="cq-09__row">
<figure class="cq-09__fig">
<div class="cq-09__card">
<p class="cq-09__eyebrow">Essay</p>
<h3 class="cq-09__title cq-09__title--cq">The quiet return of the small page</h3>
<p class="cq-09__body">Reading room design went wide for a decade. The interesting work went back to the column.</p>
</div>
<figcaption><code>clamp(1.05rem, 4cqi, 2.1rem)</code> — reads the card</figcaption>
</figure>
<figure class="cq-09__fig">
<div class="cq-09__card">
<p class="cq-09__eyebrow">Essay</p>
<h3 class="cq-09__title cq-09__title--vw">The quiet return of the small page</h3>
<p class="cq-09__body">Reading room design went wide for a decade. The interesting work went back to the column.</p>
</div>
<figcaption class="cq-09__bad"><code>clamp(1.05rem, 4vw, 2.1rem)</code> — reads the window</figcaption>
</figure>
</div>
<figure class="cq-09__fig cq-09__fig--wide">
<div class="cq-09__card cq-09__card--resize">
<p class="cq-09__eyebrow">Drag the right edge</p>
<h3 class="cq-09__title cq-09__title--cq">Every heading here is one declaration</h3>
<p class="cq-09__body">The container changes width, the type follows, and the paragraph stays in rem so text settings still apply.</p>
</div>
<figcaption>Same rule, driven by hand with <code>resize: horizontal</code></figcaption>
</figure>
</div>
</section>
```
## CSS
```css
.cq-09,
.cq-09[data-theme="light"] {
--bg: #fbfaf7;
--ink: #14120e;
--mut: #635c50;
--line: #ddd6c8;
--acc: #a86a10;
--bad: #a3341f;
}
.cq-09[data-theme="dark"] {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
@media (prefers-color-scheme: dark) {
.cq-09:not([data-theme="light"]) {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
}
@supports (color: oklch(0.5 0.1 250)) {
.cq-09,
.cq-09[data-theme="light"] {
--acc: oklch(0.58 0.12 68);
--bad: oklch(0.5 0.16 30);
}
.cq-09[data-theme="dark"] {
--acc: oklch(0.82 0.13 78);
--bad: oklch(0.72 0.14 32);
}
@media (prefers-color-scheme: dark) {
.cq-09:not([data-theme="light"]) {
--acc: oklch(0.82 0.13 78);
--bad: oklch(0.72 0.14 32);
}
}
}
.cq-09 {
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
line-height: 1.6;
}
.cq-09 *,
.cq-09 *::before,
.cq-09 *::after {
box-sizing: border-box;
}
.cq-09__stage {
max-width: 62rem;
margin-inline: auto;
padding: clamp(1.25rem, 4vw, 3rem);
display: grid;
gap: 2rem;
}
.cq-09__kicker {
margin: 0 0 0.5rem;
font-family: system-ui, sans-serif;
font-size: 0.7rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--acc);
}
.cq-09__h {
margin: 0;
font-size: clamp(1.6rem, 1.2rem + 2.4vw, 2.6rem);
font-weight: 400;
line-height: 1.15;
letter-spacing: -0.015em;
text-wrap: balance;
}
.cq-09__lede {
margin: 0.6rem 0 0;
color: var(--mut);
font-size: 1rem;
max-width: 46ch;
}
.cq-09__row {
display: flex;
flex-wrap: wrap;
gap: 2rem;
}
.cq-09__fig {
margin: 0;
flex: 0 1 17rem;
min-inline-size: 0;
display: grid;
gap: 0.6rem;
}
.cq-09__fig--wide {
flex: 1 1 100%;
}
.cq-09__fig figcaption {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.7rem;
color: var(--mut);
padding-top: 0.5rem;
border-top: 1px solid var(--line);
overflow-wrap: anywhere;
}
.cq-09__bad {
color: var(--bad);
}
.cq-09__card {
container: cq-09-card / inline-size;
padding: 1.25rem;
border-inline-start: 2px solid var(--line);
}
.cq-09__card--resize {
resize: horizontal;
overflow: auto;
min-inline-size: 12rem;
max-inline-size: 100%;
border: 1px dashed var(--line);
border-inline-start-width: 2px;
border-inline-start-style: solid;
}
.cq-09__eyebrow {
margin: 0 0 0.35rem;
font-family: system-ui, sans-serif;
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--acc);
}
.cq-09__title {
margin: 0 0 0.5rem;
font-weight: 400;
line-height: 1.12;
letter-spacing: -0.01em;
text-wrap: balance;
}
.cq-09__title--cq {
font-size: 1.35rem;
font-size: clamp(1.05rem, 4cqi, 2.1rem);
}
.cq-09__title--vw {
font-size: 1.35rem;
font-size: clamp(1.05rem, 4vw, 2.1rem);
}
.cq-09__body {
margin: 0;
font-size: 0.95rem;
color: var(--mut);
}
/* 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-09 {
position: relative;
}
.cq-09__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-09__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-09__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-09__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-09__themeopt:hover {
opacity: 0.85;
}
.cq-09__themeinput:checked + .cq-09__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.cq-09__themeinput:focus-visible + .cq-09__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.cq-09:has(#cq-09-theme-light:checked) {
--bg: #fbfaf7;
--ink: #14120e;
--mut: #635c50;
--line: #ddd6c8;
--acc: #a86a10;
--bad: #a3341f;
}
.cq-09:has(#cq-09-theme-dark:checked) {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
```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: Fluid Typography with Container Units
Source: https://codefronts.com/layouts/css-container-queries/fluid-typography-with-container-units/
Type that scales with its container instead of the window: clamp(1.05rem, 4cqi, 2.1rem) beside clamp(1.05rem, 4vw, 2.1rem) in two identical narrow cards, so the difference is visible rather than asserted. The vw version is sized by a window it knows nothing about; the cqi version is sized by the card it actually lives in.
## HTML
```html
<section class="cq-09" aria-labelledby="cq-09-h">
<fieldset class="cq-09__theme">
<legend class="cq-09__themelegend">Theme</legend>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-auto" checked>
<label class="cq-09__themeopt" for="cq-09-theme-auto">Auto</label>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-light">
<label class="cq-09__themeopt" for="cq-09-theme-light">Light</label>
<input class="cq-09__themeinput" type="radio" name="cq-09-theme" id="cq-09-theme-dark">
<label class="cq-09__themeopt" for="cq-09-theme-dark">Dark</label>
</fieldset>
<div class="cq-09__stage">
<header class="cq-09__head">
<p class="cq-09__kicker">Type & measure</p>
<h2 class="cq-09__h" id="cq-09-h">Sized by the card, not the window</h2>
<p class="cq-09__lede">Two identical cards, one declaration apart. Widen the browser and watch only one of them lose its proportions.</p>
</header>
<div class="cq-09__row">
<figure class="cq-09__fig">
<div class="cq-09__card">
<p class="cq-09__eyebrow">Essay</p>
<h3 class="cq-09__title cq-09__title--cq">The quiet return of the small page</h3>
<p class="cq-09__body">Reading room design went wide for a decade. The interesting work went back to the column.</p>
</div>
<figcaption><code>clamp(1.05rem, 4cqi, 2.1rem)</code> — reads the card</figcaption>
</figure>
<figure class="cq-09__fig">
<div class="cq-09__card">
<p class="cq-09__eyebrow">Essay</p>
<h3 class="cq-09__title cq-09__title--vw">The quiet return of the small page</h3>
<p class="cq-09__body">Reading room design went wide for a decade. The interesting work went back to the column.</p>
</div>
<figcaption class="cq-09__bad"><code>clamp(1.05rem, 4vw, 2.1rem)</code> — reads the window</figcaption>
</figure>
</div>
<figure class="cq-09__fig cq-09__fig--wide">
<div class="cq-09__card cq-09__card--resize">
<p class="cq-09__eyebrow">Drag the right edge</p>
<h3 class="cq-09__title cq-09__title--cq">Every heading here is one declaration</h3>
<p class="cq-09__body">The container changes width, the type follows, and the paragraph stays in rem so text settings still apply.</p>
</div>
<figcaption>Same rule, driven by hand with <code>resize: horizontal</code></figcaption>
</figure>
</div>
</section>
```
## CSS
```css
.cq-09,
.cq-09[data-theme="light"] {
--bg: #fbfaf7;
--ink: #14120e;
--mut: #635c50;
--line: #ddd6c8;
--acc: #a86a10;
--bad: #a3341f;
}
.cq-09[data-theme="dark"] {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
@media (prefers-color-scheme: dark) {
.cq-09:not([data-theme="light"]) {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
}
@supports (color: oklch(0.5 0.1 250)) {
.cq-09,
.cq-09[data-theme="light"] {
--acc: oklch(0.58 0.12 68);
--bad: oklch(0.5 0.16 30);
}
.cq-09[data-theme="dark"] {
--acc: oklch(0.82 0.13 78);
--bad: oklch(0.72 0.14 32);
}
@media (prefers-color-scheme: dark) {
.cq-09:not([data-theme="light"]) {
--acc: oklch(0.82 0.13 78);
--bad: oklch(0.72 0.14 32);
}
}
}
.cq-09 {
width: 100%;
min-height: 100vh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
line-height: 1.6;
}
.cq-09 *,
.cq-09 *::before,
.cq-09 *::after {
box-sizing: border-box;
}
.cq-09__stage {
max-width: 62rem;
margin-inline: auto;
padding: clamp(1.25rem, 4vw, 3rem);
display: grid;
gap: 2rem;
}
.cq-09__kicker {
margin: 0 0 0.5rem;
font-family: system-ui, sans-serif;
font-size: 0.7rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--acc);
}
.cq-09__h {
margin: 0;
font-size: clamp(1.6rem, 1.2rem + 2.4vw, 2.6rem);
font-weight: 400;
line-height: 1.15;
letter-spacing: -0.015em;
text-wrap: balance;
}
.cq-09__lede {
margin: 0.6rem 0 0;
color: var(--mut);
font-size: 1rem;
max-width: 46ch;
}
.cq-09__row {
display: flex;
flex-wrap: wrap;
gap: 2rem;
}
.cq-09__fig {
margin: 0;
flex: 0 1 17rem;
min-inline-size: 0;
display: grid;
gap: 0.6rem;
}
.cq-09__fig--wide {
flex: 1 1 100%;
}
.cq-09__fig figcaption {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.7rem;
color: var(--mut);
padding-top: 0.5rem;
border-top: 1px solid var(--line);
overflow-wrap: anywhere;
}
.cq-09__bad {
color: var(--bad);
}
.cq-09__card {
container: cq-09-card / inline-size;
padding: 1.25rem;
border-inline-start: 2px solid var(--line);
}
.cq-09__card--resize {
resize: horizontal;
overflow: auto;
min-inline-size: 12rem;
max-inline-size: 100%;
border: 1px dashed var(--line);
border-inline-start-width: 2px;
border-inline-start-style: solid;
}
.cq-09__eyebrow {
margin: 0 0 0.35rem;
font-family: system-ui, sans-serif;
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--acc);
}
.cq-09__title {
margin: 0 0 0.5rem;
font-weight: 400;
line-height: 1.12;
letter-spacing: -0.01em;
text-wrap: balance;
}
.cq-09__title--cq {
font-size: 1.35rem;
font-size: clamp(1.05rem, 4cqi, 2.1rem);
}
.cq-09__title--vw {
font-size: 1.35rem;
font-size: clamp(1.05rem, 4vw, 2.1rem);
}
.cq-09__body {
margin: 0;
font-size: 0.95rem;
color: var(--mut);
}
/* 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-09 {
position: relative;
}
.cq-09__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-09__themelegend {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-09__themeinput {
position: absolute;
inline-size: 1px;
block-size: 1px;
margin: 0;
overflow: hidden;
clip-path: inset(50%);
}
.cq-09__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-09__themeopt:hover {
opacity: 0.85;
}
.cq-09__themeinput:checked + .cq-09__themeopt {
opacity: 1;
background: color-mix(in srgb, currentColor 15%, transparent);
}
.cq-09__themeinput:focus-visible + .cq-09__themeopt {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.cq-09:has(#cq-09-theme-light:checked) {
--bg: #fbfaf7;
--ink: #14120e;
--mut: #635c50;
--line: #ddd6c8;
--acc: #a86a10;
--bad: #a3341f;
}
.cq-09:has(#cq-09-theme-dark:checked) {
--bg: #100f0c;
--ink: #f5f2ea;
--mut: #a49b8a;
--line: #34302a;
--acc: #f0b95e;
--bad: #f08a72;
}
```How this works
Swap one unit and the type starts reading the right box. Both cards below are 17rem wide and hold identical markup; only the middle term of the clamp() differs:
.card { container: cq-09-card / inline-size; }
.title--cq { font-size: clamp(1.05rem, 4cqi, 2.1rem); } /* 4% of the card */
.title--vw { font-size: clamp(1.05rem, 4vw, 2.1rem); } /* 4% of the window */vw was never the right input for a component. A heading inside a 280px card has no business being sized by a 1600px window, but that is exactly what vw does: on a wide screen it slams into the clamp() maximum regardless of how little room the card has, and on a narrow screen it undershoots in a component that happens to be wide. cqi asks the container instead, so one declaration is correct in a drawer, a grid cell and a full-bleed hero.
Keep the accessibility floor in rem. Container and viewport units both ignore the user's font-size preference, so type sized purely in cqi stops responding to browser text settings entirely. A rem minimum inside clamp() restores that floor, which is what keeps this pattern inside WCAG 2.2 SC 1.4.4. Body copy stays in rem outright here — only display type scales.
The trap: the scaling factor is relative to a container you may move. 4cqi in a 300px card is 12px; the same declaration in a 900px card is 36px. That is the feature, but it means a component dropped into a much wider container needs the clamp() ceiling checked, not just the middle term. Set the maximum from the largest placement you actually ship.
Make it yours
- Tune the middle term (
4cqi) to change how aggressively type responds; 2-5cqi covers most component type scales. - Raise the
clamp()maximum if the component appears in a very wide slot, or lower the minimum for dense UI — but keep both inrem. - Apply the same pattern to
line-heightandletter-spacingfor a proper optical scale rather than just bigger text. - Drag the third card's handle to sweep the whole range; the readout beside it names the unit doing the work.
- Scale padding with
clamp(0.75rem, 3cqi, 1.75rem)so the frame grows with the type instead of staying fixed. - Swap the serif specimen for your own face by editing
--font-displayon the root.
Gotchas — read before shipping
- Type sized only in
cqiorvwignores the user's browser font-size setting. Always keep aremfloor insideclamp(). cqiresolves against the nearest ancestor container, so adding acontainer-typebetween the heading and the intended container silently rescales the type.- Without a container ancestor at all,
cqifalls back to the small viewport size — it behaves likevwand the bug looks like nothing happened. - Very large
cqifactors make headings jump between wildly different sizes across placements; check the widest and narrowest slot you ship, not just the middle.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by :has(), oklch(), color-mix(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
cqi and the container-query machinery it resolves against are Baseline since February 2023 (Chrome 105 / Safari 16 / Firefox 110); the Chrome and Firefox floors stated here come from oklch() in the amber accent, which is behind @supports with an sRGB hex declared first. In an engine without cq units the clamp() containing cqi is invalid and the font-size declared before it — a plain rem value — is used, so the specimen renders at a sensible fixed size rather than collapsing. clamp() itself is Baseline since 2020.