Zach’s ugly mug (his face) Zach Leatherman

Obnoxiously Readable Responsive Text with Viewport Units

May 01, 2018

Ever since I created BigText over seven years ago, I’ve been a little bit obsessed with beautifully large text. Unfortunately my own blog had post titles that were—to be honest—a little bland. I wanted to spice it up a bit and move from fixed text sizes to dynamic text that grows with the viewport size. I’ve seen many iterations of this approach using resize handlers in JavaScript:

There have been other incarnations of this style of JavaScript text resizing. But as far as I can tell, we had the option to retire all of these approaches when Viewport Units were well supported in 2012! If CSS can do the job, do the job in CSS. Delete your redundant JavaScripts, everyone!

I can already anticipate the first retort to post: the JavaScript plugins can resize to element size and not viewport size! Okay—really this is just another vote for container queries. We can manage this with CSS—it just requires additional, annoyingly attentive care to maintain code for the boundaries at which our text should be resizable and when it should be fixed. But—it’s still better than a JavaScript resize-event handler (in my humble opinion). If we know where our components live in our layout, we can just adjust our Viewport Unit values accordingly to fake a sort of Container Unit.


The current layout specifications for my own blog post layout are:

  • Baseline is 100% fluid width.
  • Content has a max-width: 589px (31em at font-size: 19px) and maintains this max-width even when adding the right rail.

(This layout specification reminds me of sizes from srcset with responsive images, hmm…)

This is a pretty simple example—we have two boundaries: the breakpoint at which we’ll switch to using Viewport Units, and an upper bound when we hit the content max-width (at 589px).

This text only scales when the container width changes.

You can customize breakpoints and minimum font-size to your use case:

#demo-1 {
    /* Minimum font-size */
    font-size: 20px;

/* Arbitrary minimum breakpoint */
/* Transition from 20px minimum font-size to vw using this formula: */
@media (min-width: 320px) {
    #demo-1 {
        /* ( Minimum font-size / Breakpoint ) ✕ 100 */
        /* ( 20px / 320px ) ✕ 100 = 6.25vw */
        font-size: 6.25vw;

/* Content max-width breakpoint */
/* Transition from vw to maximum font-size using this formula: */
@media (min-width: 589px) {
    #demo-1 {
        /* Breakpoint ✕ ( Viewport Units / 100 ) */
        /* 589px ✕ ( 6.25vw / 100 ) = 36.8125px */
        font-size: 36.8125px;

Caveat: I’m using px here in a few places where I’d normally use ems or rems, to make the example code easier to read.


Transition from Fixed Minimum font-size to Viewport Units

This is used in the above example at the 320px breakpoint.

Viewport Units = ( font-size (px) / Breakpoint (px) ) ✕ 100

Transition from Viewport Units to Fixed Maximum font-size

This is used in the above example at the 589px breakpoint.

font-size (px) = Breakpoint (px) ✕ ( Viewport Units / 100 )

After you have your pixels, of course you can convert to rem or em as desired.

Smaller Delta

If you want the text to grow or shrink at a reduced rate, you can use calc to sum a vw unit with a fixed CSS unit (like px or em)—but getting your boundaries aligned properly is a bit more difficult and beyond the scope of what I’d like to cover here. An exercise left up to the reader 😇.

Update on 2018 May 25: There is an incredible article written by Florens Verschelde about The Math of CSS Locks which covers this exact subject. Seriously, go read it—it’s amazing.

Twin Props

#demo-1 {
    /* Minimum font-size */
    font-size: 20px;
    font-size: 6.25vw;

I’ve seen some developers suggest forgoing the minimum media query altogether with the above code. This is probably fine but I don’t really like that an unbounded minimum font-size could quickly become unreadable at super small breakpoints. Get on that smart watch, y’all.

Bikeshedding a New Unit

It does make me wonder how container queries (or similar) might work with Viewport Units. I’m starting to see now that the addition of a future Container Unit might be warranted to simplify the above code.

#demo-2 {
    font-size: 20px;
@media (min-width: 320px) {
    #demo-2 {
        /* Beware: `cw` is not a real unit */
        font-size: 6.25cw;

This would alleviate the need for the second breakpoint altogether, as the font-size would be determined by the size of an arbitrary container, which already has an upper bound max-width on it.


All of this build-up is really just to say that I made all my blog post titles huge and it makes me really happy. I’m using Viewport Units with a minimum boundary only 😎.

If your screen is wide enough (perhaps—say—a viewport size of 3440px), you can probably read them from space:

Giant Viewport Preview of the Blog Post Title

This is likely just the beginning of a long string of changes I’ll make to the super-wide layout for my blog.


One hefty drawback to this approach (using only vw units to scale text) noted by Andrew Romanov is that the text no longer zooms appropriately with page zoom! He proposes using calc with a combination of fixed and viewport units for at least some text zooming. Read the The Math of CSS Locks for more on that approach.

< Newer
The Simplest Web Site That Could Possibly Work Well
Older >
preload with font-display: optional is an Anti-pattern

Zach Leatherman IndieWeb Avatar for a builder for the web and the creator/maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 81 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of CloudCannon, Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »

1 Repost

IndieWeb Avatar for
Shamelessly plug your related post

These are webmentions via the IndieWeb and

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)