CSSIntermediate

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.

All CSS lessons

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), and max() 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:

  1. The slope is (48 - 24) / (1280 - 320) = 0.025, which is 2.5vw.
  2. At 320px, 2.5vw is 8px, so you need 24 - 8 = 16px extra, which is 1rem.
  3. So the preferred value is 1rem + 2.5vw, and the full rule is clamp(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 1rem to 1.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:

  1. Declare an ancestor element to be a container.
  2. Write @container rules 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-size tells the browser to track this element’s width (the “inline” direction in horizontal text).
  • container-name: product gives 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 .product styles respond to .slot, the element above it.
  • A container with container-type: inline-size cannot 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() and max(). min() is a ceiling, max() is a floor.
  • Using only vw for font sizes, which ignores zoom and font-size preferences. Add a rem part and wrap it in clamp().
  • 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 @container rules without declaring a container, so the query matches nothing.
  • Putting container-type on 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

  1. Click Practice in Editor. Resize the preview and watch the heading change smoothly. Then change the clamp() minimum to 2rem and the maximum to 4rem.
  2. Replace the clamp() on .fluid-title with a fixed font-size: 4vw. Zoom the page to 200%. What is wrong with it?
  3. Work out a clamp() that goes from 1rem at a 320px screen to 1.5rem at a 1280px screen. (Hint: slope = 0.5 / 960 px.)
  4. Build .wrap with width: min(100% - 2rem, 700px). Make the preview very narrow. Does the panel ever touch the edges?
  5. Give .panel a padding of clamp(1rem, 4vw, 3rem). How does it change as you resize?
  6. Make .slot a container and write the @container rule. Is the narrow card stacked and the wide card side by side?
  7. Change the @container breakpoint from 420px to 300px. Which slot changes now?
  8. Make the title of the product card scale with the container, using 5cqi inside a clamp().

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 rem part 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-size on 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.