skip to content

In CSS, why does width: min(100%, 60ch) behave like a maximum width rather than a minimum one?

level: middleimportance: should knowfreq 50%

answer

  1. the name describes the arithmetic
  2. smaller value wins the comparison
  3. a ceiling, not a floor
  4. equivalent to width plus max-width
  5. max() is the mirror: a floor

basics

~20 s

min() returns the smallest of its arguments at any moment, so it acts as a ceiling: the element is capped at 60ch and shrinks to 100% of its container when that is narrower. The function name describes the comparison performed, not the design intent.

solid answer

~40 s

The function names describe the arithmetic, not the role the value plays in the design. `min()` picks the smallest argument, so whichever bound is smaller wins — the element can never exceed either one, which reads as a maximum. `width: min(100%, 60ch)` therefore means "at most 60 characters wide, and never wider than the parent", the same effect as `width: 60ch; max-width: 100%` in one declaration. `max()` is the mirror image: it picks the largest argument, so it enforces a floor — `padding-inline: max(1rem, 5vw)` guarantees at least 1rem of padding and lets it grow on wide screens. The practical value is that these work in places with no `max-`/`min-` counterpart property, such as `padding`, `gap`, `margin`, or a grid track size.

go deeper

for a junior

Learn the pairing by heart: min() gives you a ceiling and max() gives you a floor. Be able to say that width: min(100%, 60ch) means at most 60ch and never wider than the parent.

for a middle

Explain that the functions are arithmetic over their arguments, evaluated continuously as layout changes, and show the longhand equivalence with width plus max-width as well as a case such as padding where no longhand exists.

for a senior

Demonstrate where these earn their keep in production CSS — bounding grid track floors so auto-fill grids do not overflow, guaranteeing minimum padding against env() insets — and be precise about what each percentage resolves against.

for a principal

Treat these as the constraint vocabulary of a sizing system: bounds expressed once inside tokens keep component authors from re-deriving measure and spacing limits per feature, and make the system's limits reviewable in a single file.

## The naming trap Every candidate who meets `min()` for the first time reads it as "the minimum size of this thing". It is not. `min()` and `max()` are arithmetic functions: they take a comma-separated list of calculations and return the smallest or the largest of them. The confusion comes from mapping the *operation* onto the *intent*, and the two are inverted: - `min(a, b)` returns the smaller value ⇒ the result can never exceed either argument ⇒ it behaves as a **maximum / ceiling**. - `max(a, b)` returns the larger value ⇒ the result can never fall below either argument ⇒ it behaves as a **minimum / floor**. ## Working the example ```css .prose { width: min(100%, 60ch); } ``` The `ch` unit is the advance width of the `0` glyph in the element's font, so `60ch` is a rough stand-in for a comfortable line length. The `100%` resolves against the containing block's width. - In a 1200px-wide container where `60ch` computes to about 550px, `min()` returns 550px. The measure is capped. - In a 400px-wide container, `100%` is 400px and is now the smaller argument, so `min()` returns 400px. The element shrinks with its parent instead of overflowing. That is exactly the pairing `width: 60ch; max-width: 100%`, collapsed into one declaration with one source of truth. ## max() as a floor ```css .section { padding-inline: max(1rem, 5vw); } ``` On a 320px viewport `5vw` is 16px, equal to `1rem`; on a 1600px viewport it is 80px. The `1rem` argument guarantees the padding never collapses to a hairline on tiny screens. A frequent production use is honouring device insets while keeping a minimum: `padding-inline: max(1rem, env(safe-area-inset-left))`. ## Why not just use min-width / max-width? Because most properties have no paired constraint property. There is no `max-padding`, no `min-gap`, no `max-margin`, and no way to bound a single grid track from a separate declaration. `min()` and `max()` bring constraint semantics to every value slot: ```css .grid { /* prevents the track floor from overflowing narrow viewports */ grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); gap: max(0.75rem, 2vw); } ``` That `minmax(min(100%, 16rem), 1fr)` is the canonical fix for `auto-fill` grids: without the inner `min()`, a 16rem track floor overflows any viewport narrower than 16rem. ## More than two arguments, and nesting Both functions accept any number of comma-separated arguments, and each argument is a full math expression, so no `calc()` wrapper is required: ```css .thing { width: min(100%, 60ch, 40rem); } .title { font-size: max(1.25rem, 1rem + 2vw); } ``` When you find yourself nesting one inside the other — `max(1rem, min(4vw, 2rem))` — that is precisely `clamp(1rem, 4vw, 2rem)`, and the `clamp()` form is easier to read. ## Pitfalls **Reaching for the function whose name matches the intent.** "I want a minimum padding" invites `min()`, which produces the opposite of the intent. Say the arithmetic out loud instead: *which value do I want to win?* **Forgetting what a percentage resolves against.** In `min(100%, 60ch)` the `100%` is a percentage of the containing block for that property — the parent's content width for `width`. Inside a flex or grid item, that containing block may not be what you assumed. **Comparing incompatible types.** All arguments must resolve to the same type. `min(100%, 60ch)` is fine because both are lengths after resolution; `min(100%, 1.5)` is invalid because a bare number is not a length. **Assuming a comparison happens once.** The comparison is re-evaluated as the layout changes, so the "winning" argument can swap as the container resizes — that is the whole point of the technique, but it means the value cannot be reasoned about from the stylesheet alone. ## How to answer it crisply "`min()` returns the smaller of its arguments, so it caps the value — it is a maximum in effect. `max()` returns the larger, so it is a floor. `width: min(100%, 60ch)` is `width: 60ch; max-width: 100%` in one declaration, and the real win is that it works on properties like `padding` and `gap` that have no `max-` counterpart."

  • What advantage does min() have over just writing width: 60ch; max-width: 100%?
    Two things. It keeps one source of truth, so a bound cannot be overridden independently and drift out of sync. More importantly it works where no paired constraint property exists — there is no `max-padding`, `min-gap` or `max-margin`, and a grid track cannot be bounded from a separate declaration, so `padding: max(1rem, 5vw)` or `minmax(min(100%, 16rem), 1fr)` have no longhand equivalent.
  • How does nesting min() inside max() relate to clamp()?
    They are the same operation. `max(A, min(B, C))` is the specification's own definition of `clamp(A, B, C)`, so any nested pair reduces to a single `clamp()` with the minimum first and the maximum last. Prefer the `clamp()` form for readability; reach for standalone `min()` or `max()` when you genuinely need only one bound.
  • Why does minmax(min(100%, 16rem), 1fr) appear so often in auto-fill grids?
    Because a bare `minmax(16rem, 1fr)` sets a track floor of 16rem, and on a viewport narrower than 16rem that floor forces horizontal overflow. Wrapping the floor in `min(100%, 16rem)` lets it collapse to the container width on very narrow screens, so the grid degrades to a single full-width column instead of scrolling sideways.

saying these in an interview costs you the question

  • Saying min() sets a minimum size for the element
  • Assuming max() caps a value at an upper bound
  • Claiming the functions only accept two arguments
  • Thinking calc() is required around arithmetic inside min() or max()
  • Believing the winning argument is chosen once at parse time

context