Layout Widths With min(), max() and clamp(): Sizing Without Breakpoints

A large share of responsive CSS is width bookkeeping: width: 100%; max-width: 72rem for a container, padding: 1rem changed to 2rem in a media query, a sidebar fixed at 18rem that must shrink on small screens, a card minimum that overflows on phones. Each is a small constraint — "as wide as possible up to a limit", "at least this much, but proportional" — and the comparison functions min(), max() and clamp() express those constraints directly in a single value. This page shows the handful of patterns that cover most layout widths and gutters, the percentage traps to avoid, and how the functions combine with grid and container units. It belongs to Intrinsic Sizing Techniques in the Mastering Container Queries & Responsive Layouts guide.

Why comparison functions instead of property pairs

width with max-width works for a block's width because CSS happens to have both properties. Most other layout values have no max- counterpart: padding, gap, grid track sizes, inset, translate, margins. Before comparison functions, bounding any of them meant media queries — a different value at each breakpoint, all tuned by hand, all wrong for containers that do not match the viewport.

min(), max() and clamp() work anywhere a length is accepted. They take a list of values, possibly in different units, and pick the smallest, the largest, or the preferred value bounded on both sides. Because they are resolved during layout against the real containing block, the result is correct in every placement, not just the ones a breakpoint anticipated.

What each function contributes min(100%, 40rem) follows the container until it reaches 40rem, then stops. max(1rem, 5%) never goes below 1rem but grows with the container. clamp(1rem, 5%, 3rem) grows with the container between a floor and a ceiling. Value against container width min(100%, 40rem) ceiling max(1rem, 5%) floor clamp(1rem, 5%, 3rem) floor and ceiling

The complete implementation

The page layout below uses comparison functions for every width and gutter. There is not a single media query, and it adapts from a phone to a wide monitor. The demo uses the same functions with smaller bounds so the effect is visible inside a small frame; drag it to see each value respond.

Live demoPage layout sized entirely with min(), max() and clamp()
Drag the frame: the sidebar shrinks within 12–18rem, gutters and gaps scale within bounds, and cards wrap without ever overflowing. Drag the bottom-right corner to resize the frame in either direction.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Layout widths with min(), max() and clamp()</title>
<style>
  body { margin: 0; font: 16px/1.6 system-ui, sans-serif; }

  /* 1. Centered container: full width until 72rem, then capped.
        One declaration replaces width + max-width. */
  .container {
    width: min(100% - 2rem, 72rem);   /* keeps a 1rem gutter each side */
    margin-inline: auto;
  }

  /* 2. Section padding: proportional, but never cramped or huge. */
  .section {
    padding-block: clamp(2rem, 6vw, 5rem);
  }

  /* 3. Two-column layout: sidebar shrinks with the page but stays within
        a readable range; main takes the rest. */
  .layout {
    display: grid;
    grid-template-columns: clamp(12rem, 25%, 18rem) 1fr;
    gap: max(1rem, 3%);
  }

  /* 4. Card grid: minimum card width that cannot overflow a narrow
        container, because min() caps the minimum at 100%. */
  .cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
    gap: clamp(0.75rem, 2vw, 1.5rem);
  }

  .card { padding: 1rem; border: 1px solid #cbd5e1; border-radius: 10px; }
  aside, main { min-width: 0; }
</style>
</head>
<body>
  <div class="container section">
    <div class="layout">
      <aside>Sidebar</aside>
      <main>
        <div class="cards">
          <div class="card">One</div><div class="card">Two</div><div class="card">Three</div>
        </div>
      </main>
    </div>
  </div>
</body>
</html>

Pattern 1 does arithmetic inside min(): 100% - 2rem is a valid argument because math functions accept calculations directly, so the container stays one rem from each edge on small screens without a separate padding rule. Pattern 4 is the most important line on the page for everyday CSS. minmax(16rem, 1fr) alone forces every track to be at least 16rem, which overflows any container narrower than that; wrapping the minimum in min(100%, 16rem) lets a single column shrink to fit. The same technique is covered from the grid side in auto-fit and minmax() Grids.

The key technique: know what each percentage refers to

Comparison functions often mix a percentage with a fixed length, and the percentage's basis decides whether the result is sensible. The basis differs by property, and the differences are not intuitive.

Percentage bases that matter for layout Width percentages resolve against the containing block's width. Padding and margin percentages resolve against the containing block's width even for top and bottom. Gap and grid track percentages resolve against the grid container's content box. Translate percentages resolve against the element's own border box. "5%" means different things in different properties width, inline-size containing block's width padding, margin (all sides) containing block's WIDTH, even vertically gap, grid tracks grid container's content box, per axis translate, transform the element's own border box Container units (cqi) avoid the ambiguity: always the query container's size.

The padding row surprises people most: padding-block: 5% is five percent of the container's width, not its height, so vertical padding grows with width. That is often what you want for section spacing — wider layouts get airier — but it makes clamp(2rem, 5%, 5rem) for vertical padding depend on the width of the parent, not the viewport. When the intent is "proportional to the viewport", use vw; when it is "proportional to the component", container query units make the reference explicit and avoid the ambiguity entirely.

Patterns that replace common media queries

A handful of idioms cover most of what media queries were historically used for in layout widths:

  • Readable content column: width: min(100% - 2rem, 65ch) — the measure caps, the gutter floors.
  • Gutters that breathe on large screens: padding-inline: max(1rem, (100% - 72rem) / 2) — at least 1rem, and on wide screens exactly enough to centre a 72rem column, which is a full-bleed-friendly alternative to a centred wrapper.
  • Sidebar that shrinks but never disappears: grid-template-columns: clamp(12rem, 25%, 18rem) 1fr.
  • Modal width: width: min(90vw, 32rem) — nearly full width on phones, fixed on desktops.
  • Icon size relative to text but bounded: inline-size: clamp(1rem, 1.25em, 2rem).

None of these need a breakpoint, and all of them work inside components placed anywhere.

Accessibility: bounds in rem, not px

The floors and ceilings in comparison functions quietly decide how the layout behaves when a reader enlarges text. A bound written in rem scales with the user's default font size and with browser zoom; a bound written in px does not respond to the font-size preference at all.

Consider clamp(12rem, 25%, 18rem) for a sidebar. A reader who sets their browser's default font to 24 pixels instead of 16 gets a sidebar floor of 288 pixels instead of 192, which keeps its enlarged text from being crushed into a narrow column. The same bounds in pixels would keep the sidebar at 192 pixels while the text inside grows by half, producing tight wrapping and possible overflow.

The same reasoning applies to gutters and gaps. A max(1rem, 3%) gap stays proportional to the reader's text, so spacing and content grow together. When a floor must be an absolute minimum for touch targets or borders, use pixels deliberately and sparingly.

At high zoom levels, percentage and viewport terms shrink relative to text, because the CSS viewport becomes narrower. Layouts built with comparison functions degrade gracefully here: the rem floors take over, columns collapse to their minimums, and wrapping or auto-fit reduces the column count. Testing at 200% and 400% zoom is still worthwhile, but these patterns rarely produce the horizontal scrolling that fixed-width layouts do under WCAG 1.4.10 Reflow.

Variation: container-aware versions

Inside a component, replacing viewport units and container percentages with container query units makes each value respond to the component's own width, which is the right reference for reusable parts.

.panel-slot { container-type: inline-size; }

.panel {
  /* Padding scales with the panel, not the page. */
  padding: clamp(0.75rem, 4cqi, 2rem);
  /* Internal columns: a label column that grows with the panel. */
  display: grid;
  grid-template-columns: clamp(6rem, 30cqi, 12rem) 1fr;
  gap: clamp(0.5rem, 2cqi, 1.25rem);
}

The same values placed in a sidebar and a wide main column now produce two different, appropriate layouts with no queries at all. Motion can follow the same principle — a component's hover travel or transition duration scaled with its container — as shown in Fluid Spacing Tokens Driving Transition Durations.

Browser support

min(), max() and clamp() are supported in Chrome and Edge 79+, Firefox 75+ and Safari 13.1+, including inside grid track definitions and with mixed units. minmax() and repeat() are supported in Chrome 57+, Edge 16+, Firefox 52+ (with repeat() from 76) and Safari 10.1+. Container query units are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+.

FAQ

Is width: min(100%, 40rem) the same as width: 100%; max-width: 40rem? For a block in normal flow, the result is the same. min() expresses it in one declaration, which also works in places where max-width is unavailable, such as grid track sizes, gap, padding and inset values.

When should I use max() instead of min()? Use max() when a value must never fall below a floor, such as gutters that should be at least 1rem even when a percentage would make them smaller: padding-inline: max(1rem, 5%). Use min() for ceilings and clamp() when you need both.

Can percentages inside min() and clamp() cause overflow? They can if the percentage basis is not what you expect. A percentage width resolves against the containing block, but a percentage padding resolves against the containing block's width even for top and bottom padding. Check which box each percentage refers to.

Do min() and max() work inside grid-template-columns? Yes, as track sizes. minmax() is still the grid function for flexible tracks with a minimum and maximum, but min() and max() are useful inside it, for example minmax(min(100%, 16rem), 1fr), which prevents a track minimum from overflowing a narrow container.

Related articles

More pages in the same section.