Code of the Day
IntermediateResponsive Design

Fluid Typography and Spacing

Fluid values scale smoothly between minimum and maximum without step-function breakpoints — the clamp() function makes this a single CSS declaration.

Web FoundationsIntermediate7 min read
Recommended first
By the end of this lesson you will be able to:
  • Explain why fixed type sizes create jumps at breakpoints
  • Use viewport units for type scaling while understanding their limits
  • Apply clamp() to create a fluid scale with a guaranteed minimum and maximum
  • Extend the pattern to padding, margin, and gap with CSS custom properties

A layout that changes at three breakpoints is responsive. A layout whose type and spacing flow between those breakpoints feels polished. The difference is clamp() — a CSS function that keeps a value between a minimum and maximum while letting it scale fluidly in between.

The problem with fixed sizes

h1 {
  font-size: 2.5rem;
}

@media (min-width: 768px) {
  h1 {
    font-size: 3.5rem;
  }
}

This works, but on a viewport just above 768px the heading jumps from 2.5rem to 3.5rem in one step. Shrink past 768px and it jumps back. Resize the browser quickly and you can see the snap. It is not wrong, but it is a step function where a slope would look better.

Viewport units for type

scale continuously with the viewport width. 1vw is one percent of the viewport width, so:

h1 {
  font-size: 5vw;
}

This scales smoothly. But the extremes are brutal: at 320px wide, 5vw is 16px — the same size as body text. At 1440px wide, it is 72px — oversized for a heading. Pure viewport units give up the concept of a minimum readable size.

clamp(): three values, one line

clamp(min, preferred, max) asks the browser to use the preferred value but never let the result fall below the minimum or rise above the maximum.

h1 {
  font-size: clamp(1.75rem, 5vw, 3.5rem);
}

Break this down:

  • 1.75rem — the minimum. Below ~560px viewport width, the heading stays at 1.75rem rather than shrinking further.
  • 5vw — the preferred. Between 560px and 1120px, the font scales with the viewport.
  • 3.5rem — the maximum. Above ~1120px, the heading stays at 3.5rem rather than becoming enormous.

No media query needed. The browser calculates which value applies based on the current viewport width.

The crossover points (where min takes over and where max takes over) are implicit: min activates when preferred would be smaller than it, and max activates when preferred would be larger. You can work them out: 1.75rem / 5vw = 35rem = 560px for the lower crossover, and 3.5rem / 5vw = 70rem = 1120px for the upper.

Sizing the preferred value

The preferred value is usually a viewport-relative expression. Common patterns:

/* Simple vw percentage */
font-size: clamp(1rem, 2.5vw, 1.5rem);

/* Mix of rem and vw — scales but keeps a stable base */
font-size: clamp(1rem, 1rem + 1.5vw, 1.75rem);

The 1rem + 1.5vw form is often smoother because it does not scale from zero — it starts at 1rem and grows by 1.5vw. The result: on a narrow screen the minimum kicks in quickly, and the scaling is gentler overall.

Applying the pattern to spacing

The same logic works for any length property — padding, margin, gap, border-radius. Fluid spacing makes the layout breathe naturally instead of snapping between fixed padding values.

.section {
  padding-block: clamp(2rem, 8vh, 6rem);
  padding-inline: clamp(1rem, 5%, 4rem);
}

padding-inline with 5% scales with the container width, keeping gutters proportional. padding-block uses vh to scale vertical breathing room with the screen height.

A fluid spacing scale with custom properties

Rather than writing clamp() everywhere, define a reusable spacing scale as CSS custom properties:

:root {
  --space-xs:  clamp(0.5rem,  1vw,  0.75rem);
  --space-sm:  clamp(0.75rem, 2vw,  1.25rem);
  --space-md:  clamp(1rem,    3vw,  2rem);
  --space-lg:  clamp(1.5rem,  5vw,  3rem);
  --space-xl:  clamp(2rem,    7vw,  5rem);
}

Then use them consistently:

.card {
  padding: var(--space-md);
  gap: var(--space-sm);
}

.hero {
  margin-block: var(--space-xl);
}

Define the scale once; update it once. Every component that references these variables responds to viewport changes automatically.

Fluid scaling is not a replacement for breakpoints — it is a complement to them. Use clamp() for continuous properties like font size and spacing. Use media queries for structural changes like column count or navigation layout. Trying to fluid-scale your way out of a layout change leads to awkward intermediate states.

Where to go next

Type and spacing scale fluidly. Next: Responsive Images — letting the browser choose the right image source for the current screen size and pixel density, without JavaScript.

Finished reading? Mark it complete to track your progress.

On this page