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-sizepreference 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.
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
emunits, which are based on the user preference (and responsive to zoom). - The additional
vwvalue 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 effectivefont-sizevalue of200%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.