One of the most exciting aspects of web development in recent years has been the continued growth and improvement of CSS. Flexbox and grid revolutionized how we build webpage layouts. Custom properties (aka, CSS variables) opened up new possibilities for theming. Newer selectors like
:where() have lead to more powerful and concise CSS. Container queries are now a reality and looking ahead, native CSS nesting is on its way (decisions concerning its syntax notwithstanding).
But all of these new and upcoming features means that CSS is becoming increasingly complicated. Inevitably, then, some new features might fall through the cracks and get overlooked. (Which is why I’m of the opinion that “full-time CSS engineer” ought to be a thing, but that’s a topic for another post.) Speaking personally, I’m still pretty ignorant concerning the benefits of newer color formats like
lch(). Which brings me to viewport units.
Put simply, viewport units allow you to size a page’s elements relative to the size of the browser’s viewport, which contains everything that is currently visible on a webpage. (The viewport is basically the browser window minus any UI elements like the navigation and search bar.)
Consider this very simple example:
vh stands for “viewport height,” so an element set to
100vh will be 100% of the viewport’s height. If that element’s height is set to
50vh, then it’ll be 50% of the viewport’s height, and so on. The
100vh is often used when you want an element to fill up the entire browser window. For instance, I use this technique on special features (like my recent David Zindell post) to make their headers fill up the entire viewport with a splashy image. This gives them some extra visual oomph that sets them apart from my “normal” posts.
Not All Viewports Are the Same
This approach works well except for one pretty prominent scenario. If you’re viewing such a layout in Safari on an iOS device, that
100vh element fills up the viewport, but its bottom portion is then covered by a toolbar that includes the next/previous navigation and other controls. (See Figure A.)
Note: Although I’m focusing on iOS Safari, this issue also occurs in iOS Chrome. It doesn’t occur in other iOS browsers like Brave, DuckDuckGo, Firefox, and Opera. (More on that in a moment.) I haven’t tested this in any Android browsers.
In other words, Safari doesn’t seem to take its own UI into consideration when drawing its viewport. Thus, a
100vh element doesn’t behave the way it seems like it should, i.e., filling up the space between the URL bar and the bottom toolbar. (Remember that a browser viewport is the browser window minus any UI elements.)
There are, of course, reasons for why Apple opted for this approach. And reading the developer’s explanation — the viewport’s height changes dynamically because any toolbars disappear or minimize when you scroll — they seem perfectly valid. But that doesn’t mean I liked how it looked. It was hard to believe that this was still an issue the Year of Our Lord 2023.
That aforelinked article also pointed me to Matt Smith’s article about
-webkit-fill-available, which seemed promising at first. Unfortunately, it wasn’t applicable to my situation. I didn’t want the post header to simply fill up the entire available space because I also needed to take into account the height of my site’s header, which contains the logo, nav, and search.
Here’s what my original CSS looked like:
The site header is 6 rems high, so I use the
calc function to subtract that from the
100vh to dynamically calculate the post header’s new height. But, as pointed out before, iOS doesn’t respond to
100vh the way you might think it would. What I really needed was a new type of CSS unit — and fortunately, I found it.
New Viewport Units
Back in November, Google’s Web.dev blog covered three new viewport units: the “large,” “small,” and “dynamic” viewport units. These units were created specifically to work with viewports whose size might change due to dynamic toolbars — which was the exact problem I was facing.
- The “large” viewport units assume that any dynamic toolbars (e.g., Safari’s bottom bar) are retracted and hidden, and calculate the viewport’s size accordingly. (This is akin to Safari’s aforementioned default behavior.)
- The “small” viewport units assume that any dynamic toolbars are expanded and visible, and calculates the viewport’s size accordingly.
- The “dynamic” viewport units sit in-between the “large” and “small” units, and react automatically to the dynamic toolbar’s behavior.
At first glance, a “dynamic” viewport unit seemed like the solution. After all, who doesn’t like a web design that automatically responds, all on its own, to a given situation? With that thought in mind, I updated my CSS:
In addition to the original selector, I added a feature query via
@supports that basically says if the browser recognizes and supports the
height: 100dvh declaration, then run the following CSS. (This is an example of progressive enhancement, i.e., starting with the basics and then adding on more advanced code that modern browsers will recognize.) That CSS is virtually identical to my original CSS, except I’m now using
100dvh instead of
dvh stands for “dynamic viewport height.”)
The first time I loaded the page, the problem seemed to be solved: the post header now filled up the space between Safari’s toolbars without anything cut off or hidden. But then I scrolled a little bit.
When you scroll down in iOS, the browser’s toolbars disappear or reduce in size, thus increasing the height of the browser’s viewport. Conversely, scrolling back to the top causes the toolbars to reappear or return to their original size, thus decreasing the viewport’s height. This behavior caused some distracting (IMO) changes to the post header: the background image expanded while the text shifted down in response to the additional height.
Interestingly, this “dynamic” approach is the behavior employed by the iOS versions of Brave, DuckDuckGo, Firefox, and Opera. In other words, toolbar overlap appears to be a non-issue for them, at least as far as Opus is concerned.
So after giving it some more thought, I replaced
100svh — i.e., the “small” viewport height — which assumes that any toolbars are always expanded.
Here’s my final code:
You can see the results — that is, the entire post header — in Figure B. Upon scrolling, the post header doesn’t take advantage of the increased viewport height, so it’s not a truly “full-height” element. However, it doesn’t have any weird shifting, either, but looks the same all the time. And I always prefer such stability in my web designs.
For what it’s worth, Firefox, Brave, et al. ignore the
100svh setting altogether, and instead, always stick with the “dynamic” handling of the viewport and post header heights. That’s a little frustrating, but since they represent a relatively minuscule amount of Opus’ overall traffic, I’m not going to sweat it.
Along with the aforementioned color formats, viewport units are one of those aspects of CSS that has always felt rather abstract to me. (Then again, I still have trouble wrapping my mind around how
srcset works, and that’s used all the time for responsive images.) The problems they seek to address have often seemed rather niche to me, compared to the issues that I’m trying to solve 95% of the time.
Of course, now I have to eat a little crow because I found myself in just such a “niche” situation. Which is to say, I’m glad that really smart people have spent time thinking through these situations, rarefied as they might seem, to find and propose potential solutions.
I’m also glad that browser makers are quick to implement them; browser support for these new viewport units is pretty good, with Opera being the only major holdout. (Which means that I’ll probably remove the
@supports feature query in the not-too-distant future and use the
100svh as the default CSS.)
Finally, while Safari’s behavior was initially frustrating, I do believe they made the better choice concerning how to handle dynamic toolbars and viewport heights now that I’ve seen how Firefox et al. handle them. I’d rather have part of the design covered up by default (but fixable, if needed, with the right CSS) then see the page rearrange itself as you scroll. The latter behavior is unexpected and thus distracting, two things that can create a poorer user experience — which is something I try to avoid in every aspect of my designs.