Responsive and fluid typography with Baseline CSS features

Published: December 16, 2025

Responsive web design is an approach to building websites that look good and function well across a range of different browsers, viewport dimensions, devices, and user preferences. When applied to typography, the central concern is often adjusting font-size based on the browser width. This can impact spacing values, such as line-height and margin.

For designers and developers, it makes sense to think about what space is available in the browser and adjust your typography accordingly. And, it's important to remember that users may have different font-size needs across a range of devices, reflecting personal circumstances outside your reach or awareness. It's a bad choice to make changes that removes user control of the final result.

There are two primary inputs that for people to customize font size while they browse the web:

  • Providing a default font-size preference across all websites.
  • Zooming in or out on a site-by-site basis.

The demo's goal is to make typography responsive to both the browser's viewport size and user inputs. But you must understand that the more your typography responds to viewport size, the less it responds to user preferences. If you implement responsive typography, it's important to do so carefully, with respect for accessibility.

Negotiate a base font-size based on user preferences

The first step for defining any typography online is to negotiate an initial font size based on the user's font-size preference. This is used for the majority of the text on the page and as a basis for other font sizes like headings. The simplest option here is to give the user full control by using 1em, without any adjustments. When you don't set any other font-size value, 1em refers to the user preference. On the other end of the spectrum, setting a font-size in pixels or other absolute units (and even viewport-relative units) overrides the user entirely, which you should avoid.

However, different use cases call for different typography. An article might be easier to read in large text, while data-heavy sites might call for a more compact design with smaller text. In either case, you might want to suggest a default that fits the design while still letting the user adjust the result based on their unique situation.

Option one: calculate a multiplier based on assumptions

One common compromise is to define an adjusted font-size in em or % units, relative to the user default font-size. Generally, this approach starts with the assumption that browsers provide a 16px default, and most users leave that default in place. If you think a 20px font size works better for your site, then a font-size of 1.25em or 125% likely gives the result you want:

html {
  /* 20px preferred, 16px expected: 20/16 = 1.25 */
  font-size: 1.25em;
}

You can use a calc() function to show the math, but you still have to know what the equation is: the target size, divided by the expected size, multiplied by 1em.

html {
  font-size: calc(20 / 16 * 1em);
}

Users with a larger or smaller preference can impact the result, as your default is now relative to theirs. In this example, 1.25 times their preference. But, it can be strange if both you and the user are requesting 20px defaults, and the result is 25px, their adjusted default times 1.25 again.

Option two: let clamp() do the work

A more nuanced approach involves CSS comparison functions, without any math. Rather than assuming 1em is equal to 16px and doing unreliable conversions from px to em, you can think of 1em as a variable that refers to the user preference. No matter what pixel value 1em represents, a font-size of max(1em, 20px) always returns the larger of your design preference (20px) and the user's preference (1em). This allows the user choose larger, but not smaller, font sizes.

By switching to a clamp() function, you can help users scale when their preferred size is too far from your chosen default. For example, a font-size of clamp(1em, 20px, 1.25em) defaults to 20px as long as that is larger than the user default, but no more than 125% of their default.

This means your design takes priority when it's close to the user preference, but the user preference retains priority when it's outside the specified range. There's no conversion math involved, no assumptions about the user preference size, and no multiplication of designer and user values.

By setting this as the root font-size on the html element, you can now reference 1rem anywhere on the site as the negotiated base size.

Add responsiveness

To make this font-size responsive to the viewport, one option would be to add media query (or container query) breakpoints. For example, you could change the clamped value depending on the screen size:

html {
  font-size: clamp(1em, var(--base-font-size, 16px), 1.25em);
  @media (width > 30em) { --base-font-size: 18px; }
  @media (width > 45em) { --base-font-size: 20px; }
}

The other option is adding viewport or container units to the static base value:

html {
  font-size: clamp(1em, 16px + 0.25vw, 1.25em);
}

The vw (viewport width) or vi (viewport inline size) units represent 1% of the total viewport, the part of the browser that renders your site. Similarly, the cqw and cqi units represent 1% of an inline-size container on the page. Check out the container queries and units demo for more details.

This approach is often referred to as fluid typography, since the change in font-size is constant over a range of viewport widths, rather than jumping from one value to another at media or container breakpoints. However, don't get distracted by the smoothness of the transition—that distinction is generally only visible in testing, if you smoothly adjust the window size. That effect is rarely (if ever) seen by users. While users may regularly change the size of the browser or the zoom level, they would have to make those adjustments in a slow and fluid manner to notice the difference between a breakpoint and a viewport unit. It only impacts the transition, not the result after resizing.

The main advantage of fluid font sizing is that it removes any need to calculate or specify breakpoints manually, providing an interpolated result at any given size. You set the starting point (such as 16px) and the rate of change (for example, 0.25vw provides a 0.25px increase in font-size for every 100px increase in the viewport), and possibly min and max values. When the viewport is 1000px wide, the font-size is 16px + 2.5px or 18.5px. The calculation is handled entirely by the browser. This is the approach used in the demo, using cqi units to show container-based responsiveness. When used on the root (html) element, where there is no defined container, cqi units still refer to the viewport size.

If you prefer to think in terms of the specified font-size at a given viewport size, consider using the more direct media query approach, which is a bit more clear. Things get complicated when you try to calculate viewport units based on intended breakpoints. Many people do this by copy and pasting values from third-party tools, but the resulting code is much more difficult to understand or change directly. In general with CSS, the best option is the one that most clearly expresses your intentions.

Warning: Viewport changes may apply in different ways

While media queries and vi units are applied in different ways, both approaches are based on the same measurement of the viewport. If the viewport is 600px wide, then 100vw is equal to 600px, and styles inside the (width > 500px) media query are applied.

But what does it mean for the viewport to be 600px wide? A pixel is more complicated than a fixed size with a single meaning. There's many reasons why the number of pixels across the viewport might change:

  • Physical screen size: Smaller screens and narrow browser windows may have fewer pixels. However, some devices may be smaller but have a higher pixel density than a larger device.
  • Zooming and resizing: Changing the zoom level has the same impact on the measured viewport width as resizing the browser window. Zooming changes the size of a pixel, while resizing changes the size of the browser. Both change the number of pixels across the width of the browser.

What we get from the viewport measurement is a relationship between the current pixel size and the current browser window.

Zooming and resizing serve different purposes. A user that changes the zoom level is trying to make the contents of the page larger or smaller, while a user that resizes the browser is managing space across a screen. Even though the user intent is different, the result on CSS measurements is the same. As the window gets smaller, or the pixel gets larger, there are fewer pixels across the browser's width.

That disconnect makes responsive typography unreliable. If your text is set to resize based only on a viewport or container, then user zoom has no effect.

Changing the value of the viewport-relative unit to 1vw or 100vw changes the exact relationship between the font size and viewport. A 1vw font grows 1px for every 100px of viewport size, while a 100vw font is the same size as the viewport. You can change that value to make the font grow more slowly or more quickly in relation to the browser. But any viewport-relative value remains constant as the user zooms in or out, entirely unresponsive to user controls.

Similarly, neither 1vw nor 100vw accounts for the user default font-size.

Using viewport or container-relative units on their own for font-size is always hostile to the user. When a font-size is fully responsive to its container, it cannot also be responsive to user defaults or adjustments. Even with the best intentions and safeguards, taking that final font-size control away from users should be avoided. It's a bad user experience, and can break accessibility guidance that is required by law. Specifically, section 1.4.4 of the Web Content Accessibility Guidelines requires that "text can be resized without assistive technology up to 200 percent without loss of content or functionality."

Ensure font-size values are responsive to zoom

To ensure that a viewport-relative font-size is responsive to zoom, the viewport-relative value has to be applied as an adjustment to some other value. That's possible in CSS using the calc() function or any other math function that accepts calculations, such as min(), max(), and clamp(). A font-size of calc(16px + 1vw) is based on both the viewport size, and also the current (zoom-relative) size of a pixel. While the vw unit won't be impacted by zoom, the base value is.

The result is a font-size that responds to both the viewport size and the user's zoom settings. If the user zooms to 200%, the base value renders twice as large (32px), while the responsive value remains unchanged. A 1000px viewport would initially give you a font-size of 16px + 10px = 26px, but at 200% zoom, the font size would only grow to 42px, just over 160%. That may not seem like an extreme issue, but the more your font-size is based on the viewport, the less effective zoom becomes.

On small screens, the font-size is derived from the base pixel value and responds well to zoom. On larger screens, the viewport-sizing becomes a larger fraction of the rendered font size, which makes zoom less effective. This is particularly harmful at the point when the text has been enlarged 500%, the max in most browsers, so text can't be further resized so the content is readable, as required by WCAG 1.4.4. Even before that point, it can be frustrating to have zoom become ineffective.

A graph showing font size and zoom effectiveness versus viewport width. The font size, calculated as `calc(17px + 2.5vw)`, increases linearly with viewport width. The 500% zoom line, representing the maximum possible zoom, shows that zoom becomes less effective as viewport width increases, failing to provide a 200% font size increase beyond a viewport width of 2040px.
The horizontal axis represents viewport size, from 0 to 2600px wide. The vertical axis for font-size is also in pixels, showing the result of calc(17px + 2.5vw). The 500% zoom line uses the same viewport-width horizontal axis, but treats the vertical axis as a percentage.

At the left edge of the graph (0 viewport width), 500% zoom is fully effective. However, that effectiveness drops away quickly as the browser size increases, and (unzoomable) viewport units become a larger factor in the font-size. When the browser is 2040px wide, the maximum 500% zoom is only able to achieve a 200% increase in font-size. Beyond that point, 200% effective font-zoom is no longer possible.

By moving this calculation into a clamp() function, with min and max values, you can enforce boundaries that ensure zoomable text. According to Maxwell Barvian:

If the maximum font size is less than or equal to 2.5 times the minimum font size, then the text will always pass WCAG SC 1.4.4, at least on all modern browsers.

Since @media and @container queries are based on the same measurements as vw and cqw units, the same logic applies when using a breakpoint to change the font size. When the size increase is too dramatic, zoom becomes ineffective. You can experiment with how these values interact in the following visualization:

Ensure font-size values are responsive to user defaults

But calc(16px + 1vw) is still not responsive to user default font settings. To achieve that, you can set a base or min and max values using em or rem units rather than px. Putting it all together, you get a familiar result that matches the linked demo:

html {
  font-size: clamp(1em, 17px + 0.24vw, 1.125em);
}

Note that:

  • The minimum and maximum both use em units, which are based on the user preference (and responsive to zoom).
  • The additional vw value is kept minimal, so that zoom isn't too impacted.
  • The maximum size (1.125em) is well under 2.5 times the minimum (1em), ensuring that an effective font-size value of 200% is always possible.

Typographic scales with pow()

Most designs use more than one font size! A typographic scale describes the relationship between multiple font sizes. This can be expressed as a base size and a series of multipliers to calculate the other sizes. CSS provides a built-in typographic scale relative to the medium keyword, which refers to the user's font size preference, or a default of 16px. The full keyword scale is:

  • xx-small: 3/5 (0.6)
  • x-small: 3/4 (0.75)
  • small: 8/9 (0.89)
  • medium: 1 (the base size that others are multiplied against)
  • large: 6/5 (1.2)
  • x-large: 3/2 (1.5)
  • xx-large: 2/1 (2)
  • xxx-large: 3/1 (3)

This scale is relative to the user's default rather than the root font-size. This means it won't work as well once you've changed the root font-size of your site. Most authors end up re-creating a similar type scale with custom properties. Sometimes they use the same t-shirt size names, and sometimes they use a series of steps up and down a mathematical scale. There are many third-party tools for generating these scales based on common ratios, mostly borrowed from a western musical scale:

html {
  /* musical ratios */
  --minor-second: calc(16/15);
  --major-second: calc(9/8);
  --minor-third: calc(6/5);
  --major-third: calc(5/4);
  --perfect-fourth: calc(4/3);
  --augmented-fourth: sqrt(2);
  --perfect-fifth: calc(3/2);
  --major-sixth: calc(5/3);

  /* the golden ratio*/
  --golden-ratio: calc((1 + sqrt(5)) / 2);
}

You don't need outside tools to create your own scale in CSS. The pow() function can generate your scale for you, with 1rem as your own base size.

html {
  /* choose a ratio */
  --scale: 1.2;

  /* generate the scale using pow() */
  --xx-small: calc(1rem * pow(var(--scale), -0.5));
  --x-small: calc(1rem * pow(var(--scale), -0.25));
  --small: calc(1rem * pow(var(--scale), -0.125));
  --medium: 1rem;
  --large: calc(1rem * pow(var(--scale), 1));
  --x-large: calc(1rem * pow(var(--scale), 2));
  --xx-large: calc(1rem * pow(var(--scale), 3));
  --xxx-large: calc(1rem * pow(var(--scale), 4));

  /* change the ratio for different viewport sizes */
  @media (width > 50em) {
    --scale: var(--perfect-fourth);
  }
}

You can use whole numbers or fractions, while keeping the scale consistent. The common 12pt typography scale uses roughly 5 fractions per step. While the large sizes here use whole steps in the scale, the small sizes use fractions to scale at a slower rate.

CSS mixins and functions make it possible to condense that logic even more, while other built-in tools like progress() make it easier to create scales that fluidly adjust from one value to another. But those features are out of scope for this demo.

Respond to the size of in-page containers

You can use the cqi unit instead of vw or vi to make these calculations work in container queries. Leave the root font-size on the html element so that every container can reference that preference as 1rem. In the demo, the full type scale applies to the body rather than the root html element, and then resets based on the container size for elements with the type-set attribute.

Container-relative font sizes have a trade-off. This method gives you fluid font sizing for each element in context at the cost of page-wide consistency. Your use case determines which factor matters most. Fluid typography also makes user controls like zoom less effective.

Responsive typography and typographic scales are useful tools, but you don't need them if they complicate your design. The user default and built-in type scale work well too. If you choose responsive typography, test how it behaves with different user defaults and zoom settings.