Fluid Type with clamp(), min(), max() and Container Queries
Make text and spacing scale smoothly with clamp(), min(), max() and calc(), then build components that adapt to their own space using container queries.
What you will learn
In the last lesson, you made layouts adapt using breakpoints: sudden changes at certain screen widths. This lesson shows two newer, smoother tools. First, the CSS math functions clamp(), min(), max() and calc(), which let sizes and spacing scale smoothly with the screen instead of jumping. Second, container queries, which let a component respond to the space it has, rather than the size of the whole browser window. Together they let you write far fewer breakpoints and build components that work anywhere.
The HTML we will style
<h1 class="page-title">Fluid Lab</h1>
<!-- Part 1: fluid typography -->
<section class="fluid-section">
<h2 class="fluid-title">A heading that grows with the screen</h2>
<p class="fluid-text">Resize the preview. The heading and this paragraph change size smoothly...</p>
</section>
<!-- Part 2: fluid width and spacing -->
<div class="wrap">
<div class="panel">
<h3 class="panel-title">A fluid panel</h3>
<p class="panel-text">Its width never exceeds 700px...</p>
</div>
</div>
<!-- Part 3: container queries. The SAME card in two different containers -->
<div class="demo">
<div class="slot slot-narrow">
<article class="product">
<div class="product-image">Image</div>
<div class="product-info">
<h3 class="product-title">CSS Course</h3>
<p class="product-text">Learn styling from the basics to layouts.</p>
<a href="#start" class="product-link">Start learning</a>
</div>
</article>
</div>
<div class="slot slot-wide">
<!-- exactly the same <article class="product"> again -->
</div>
</div>
| Selector | Element | Used for |
|---|---|---|
.fluid-section |
the <section> in Part 1 |
groups the fluid text |
.fluid-title, .fluid-text |
the <h2> and the <p> |
fluid typography with clamp() |
.wrap |
the outer <div> in Part 2 |
a centred, width-limited wrapper with min() |
.panel, .panel-title, .panel-text |
the box inside .wrap |
fluid padding with clamp() |
.demo |
the <div> that holds both slots |
(just a wrapper) |
.slot, .slot-narrow, .slot-wide |
two containers of different widths | the query containers |
.product |
the <article> in each slot |
the component that adapts |
.product-image, .product-info |
the picture box and the text box | the two parts that change position |
.product-title, .product-text, .product-link |
text, inside .product-info |
text styling |
Notice that Part 3 has the same HTML twice, in two containers of different sizes. That is the whole point of container queries.
Part 1: CSS math functions
CSS can calculate values for you. Four functions cover almost every case.
calc(): do the maths
calc() lets you mix units in one expression:
.panel {
width: calc(100% - 40px); /* the full width minus a fixed 40px */
}
.fluid-section {
padding: calc(1rem + 2vw);
}
Two rules: you need spaces around + and - (100%-280px is invalid), and for * and / at least one side must be a plain number.
min() and max(): pick the smaller or larger
/* the <div class="wrap"> */
.wrap {
width: min(100% - 2rem, 700px);
margin: 0 auto;
}
min() takes a list and uses the smallest value. This reads as “700px wide, but never wider than the screen minus 2rem”, which is exactly max-width: 700px plus side margins, in one line. Inside these functions you do the maths directly, without writing calc().
max() uses the largest value:
.product-link {
display: inline-block;
padding: max(12px, 1.5vw); /* at least 12px, bigger on wide screens */
}
A common point of confusion:
min()acts like an upper limit (a cap), andmax()acts like a lower limit (a floor).min(100%, 700px)can never be more than 700px.max(12px, 1.5vw)can never be less than 12px.
clamp(): a value with a floor and a ceiling
clamp() is the star. It takes three values, minimum, preferred, maximum:
font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);
The browser uses the preferred value in the middle, but never goes below the minimum or above the maximum. In other words, it is a shortcut for max(min-value, min(preferred, max-value)).
Fluid typography
On a phone, a 3rem heading is huge. On a desktop, a 1.5rem heading is tiny. The old solution was three font sizes with two breakpoints. With clamp(), you write one line and the heading scales smoothly:
/* the <h2 class="fluid-title"> */
.fluid-title {
font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);
line-height: 1.2;
}
/* the <p class="fluid-text"> */
.fluid-text {
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
}
Here is what the title does at different screen widths (assuming a 16px root size):
| Viewport width | Preferred value 1rem + 2.5vw |
Result |
|---|---|---|
| 320px | 16 + 8 = 24px | 24px (the 1.5rem minimum) |
| 480px | 16 + 12 = 28px | 28px |
| 800px | 16 + 20 = 36px | 36px |
| 1200px | 16 + 30 = 46px | 46px |
| 1400px | 16 + 35 = 51px | 48px (capped at the 3rem maximum) |
The text grows steadily from 24px up to 48px, then stops.
Why not just vw?
You could write font-size: 4vw, but there are two serious problems. It becomes absurdly small on phones and absurdly large on wide monitors, and, more importantly, it ignores the visitor’s zoom and font-size settings, so people who need larger text cannot get it. This is an accessibility failure. Mixing a rem part into the preferred value (1rem + 2.5vw) keeps the text responsive to the visitor’s settings, and the clamp() limits prevent extremes.
Working out your own values
To go from 24px at a 320px screen to 48px at a 1280px screen:
- The slope is
(48 - 24) / (1280 - 320) = 0.025, which is2.5vw. - At 320px,
2.5vwis 8px, so you need24 - 8 = 16pxextra, which is1rem. - So the preferred value is
1rem + 2.5vw, and the full rule isclamp(1.5rem, 1rem + 2.5vw, 3rem).
That is exactly the rule above. There are online clamp calculators that do the same maths, but it helps to know what they are doing.
Fluid spacing and sizes
Anything that takes a length can use clamp(), not only text:
/* the <div class="panel"> */
.panel {
padding: clamp(1rem, 4vw, 3rem); /* tight on phones, roomy on desktops */
border: 1px solid #dddddd;
border-radius: 12px;
}
.demo {
display: grid;
gap: clamp(0.75rem, 2vw, 2rem);
}
Combine this with the min() wrapper from earlier and you have a layout with no media queries at all that still behaves well from phones to monitors:
.wrap {
width: min(100% - 2rem, 700px);
margin: 0 auto;
}
Use it with judgement. Fluid values are great for headings, section padding and gaps. For body text, a small range (like
1remto1.25rem) is enough. Do not let important text get too small or too large.
Part 2: Container queries
Media queries ask: “How wide is the browser window?” That is a problem for components. Imagine a product card that you reuse in a wide main column and in a narrow sidebar. A media query cannot tell the difference, because the window is the same width in both places. The card needs to ask: “How wide is the space I am in?”
That is exactly what a container query does. You do two things:
- Declare an ancestor element to be a container.
- Write
@containerrules that look at that container’s size.
Step 1: declare the container
/* both <div class="slot"> elements */
.slot {
container-type: inline-size;
container-name: product;
}
container-type: inline-sizetells the browser to track this element’s width (the “inline” direction in horizontal text).container-name: productgives it a name, so queries can target it precisely. The name is optional but useful.- A shorthand exists:
container: product / inline-size;.
In our page the two slots only differ in width:
.slot {
container-type: inline-size;
container-name: product;
margin-bottom: 20px;
padding: 12px;
border: 2px dashed #94a3b8;
}
.slot-narrow { max-width: 300px; }
.slot-wide { max-width: 640px; }
Step 2: write the @container rule
The component starts with its simple, narrow layout: image on top, text below.
/* each <article class="product"> */
.product {
display: grid;
gap: 12px;
}
/* the <div class="product-image"> */
.product-image {
height: 120px;
display: grid;
place-items: center;
background-color: royalblue;
color: white;
font-weight: bold;
}
.product-title {
margin: 0 0 4px;
}
.product-text {
margin: 0 0 8px;
}
.product-link {
color: royalblue;
font-weight: bold;
text-decoration: none;
}
Then, when its container is wide enough, the layout changes to image beside text:
@container product (min-width: 420px) {
.product {
grid-template-columns: 160px 1fr; /* image on the left, text on the right */
}
.product-image {
height: auto;
}
.product-title {
font-size: 1.5rem;
}
}
The rule is @container name (condition) { rules }. It looks just like a media query, but the condition compares against the named container’s width.
In the preview, the narrow slot (300px) stays stacked, while the wide slot (640px) switches to the side-by-side layout, on the same screen, at the same time, with the same CSS and HTML. That would be impossible with media queries. Put this card in a sidebar, a modal, a grid cell or a full-width section, and it adapts to each.
If both slots look alike, make the preview wider: the wide slot needs at least 420px of room.
Rules and limits
- The query looks at an ancestor. An element can never be queried by its own
container-type. That is why.productstyles respond to.slot, the element above it. - A container with
container-type: inline-sizecannot depend on its content for its width, so it needs a width from its context (a normal block element is fine). A shrink-to-fit container, such as a bare inline-block, can collapse to zero width. - Container queries work in all current major browsers.
- You can also query other conditions, such as
(max-width: 300px)or ranges like(300px < width < 600px).
Container units
Inside a container, a few units measure the container, not the screen:
| Unit | Meaning |
|---|---|
1cqw |
1% of the container’s width |
1cqi |
1% of the container’s inline size (the same thing in horizontal text) |
These make the type of a component scale with its own space:
.product-title {
font-size: clamp(1.1rem, 5cqi, 1.6rem);
}
Media queries or container queries?
| Use a media query for… | Use a container query for… |
|---|---|
| the overall page layout (header, columns, sidebar) | components (cards, widgets, form groups) |
| user preferences (dark mode, reduced motion, print) | anything that is reused in different-sized places |
| anything tied to the whole screen | a component whose space does not match the screen |
The two work together. Use media queries for the page, and container queries for the pieces.
Putting it together
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: Arial, Helvetica, sans-serif;
line-height: 1.6;
}
/* Part 1: fluid type */
.fluid-title {
font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);
line-height: 1.2;
}
.fluid-text {
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
}
/* Part 2: fluid width and spacing */
.wrap {
width: min(100% - 2rem, 700px);
margin: 0 auto;
}
.panel {
padding: clamp(1rem, 4vw, 3rem);
border: 1px solid #dddddd;
border-radius: 12px;
}
/* Part 3: container queries */
.slot {
container-type: inline-size;
container-name: product;
margin-bottom: 20px;
padding: 12px;
border: 2px dashed #94a3b8;
}
.slot-narrow { max-width: 300px; }
.slot-wide { max-width: 640px; }
.product { display: grid; gap: 12px; }
.product-image {
height: 120px;
display: grid;
place-items: center;
background-color: royalblue;
color: white;
font-weight: bold;
}
.product-title { margin: 0 0 4px; }
.product-text { margin: 0 0 8px; }
.product-link { color: royalblue; font-weight: bold; text-decoration: none; }
@container product (min-width: 420px) {
.product { grid-template-columns: 160px 1fr; }
.product-image { height: auto; }
.product-title { font-size: 1.5rem; }
}
Common mistakes
- Writing
calc(100%-280px)with no spaces around the minus sign. - Mixing up
min()andmax().min()is a ceiling,max()is a floor. - Using only
vwfor font sizes, which ignores zoom and font-size preferences. Add arempart and wrap it inclamp(). - Putting the arguments of
clamp()in the wrong order. It is minimum, preferred, maximum. - Choosing a minimum bigger than the maximum. The minimum wins and the value never changes.
- Over-fluid text. Huge ranges make text too small on phones or too large on monitors.
- Writing
@containerrules without declaring a container, so the query matches nothing. - Putting
container-typeon the same element that you try to restyle. The styles must target a descendant of the container. - Using a shrink-to-fit container (inline-block or an unsized flex item) with
inline-size, so it collapses. - Replacing every media query with a container query. Page-level layout and user preferences still need media queries.
Practice
- Click Practice in Editor. Resize the preview and watch the heading change smoothly. Then change the
clamp()minimum to2remand the maximum to4rem. - Replace the
clamp()on.fluid-titlewith a fixedfont-size: 4vw. Zoom the page to 200%. What is wrong with it? - Work out a
clamp()that goes from1remat a 320px screen to1.5remat a 1280px screen. (Hint: slope = 0.5 / 960 px.) - Build
.wrapwithwidth: min(100% - 2rem, 700px). Make the preview very narrow. Does the panel ever touch the edges? - Give
.panelapaddingofclamp(1rem, 4vw, 3rem). How does it change as you resize? - Make
.slota container and write the@containerrule. Is the narrow card stacked and the wide card side by side? - Change the
@containerbreakpoint from420pxto300px. Which slot changes now? - Make the title of the product card scale with the container, using
5cqiinside aclamp().
Recap
calc()does maths with mixed units (spaces around+and-).min()picks the smaller value (a cap),max()the larger (a floor).clamp(min, preferred, max)keeps a value between two limits.font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem)makes text fluid with one line.- Include a
rempart in fluid sizes so they respect zoom and the visitor’s text settings. width: min(100% - 2rem, 700px)is a centred, responsive wrapper in one line.clamp()also works for padding and gaps.- Container queries let a component adapt to the space of its container: set
container-type: inline-sizeon an ancestor, then write@container name (min-width: ...). - Use media queries for page layout and user preferences, and container queries for reusable components.
- You now have the full responsive toolkit: flexible layouts, breakpoints, fluid values and container queries.