Prevent content from being hidden underneath the Virtual Keyboard by means of the VirtualKeyboard API

One of the problems on mobile devices is that the keyboard can hide some of your content. The VirtualKeyboard API aims to solve this.

~

The Problem

The current behavior on mobile devices is that as the keyboard gets shown, the Layout Viewport remains the same size but the Visual Viewport shrinks. Because layout items with position: fixed; bottom: 0; get anchored to the Layout Viewport, these items can become hidden underneath the Virtual Keyboard (or bleed out from the top in case they use top: 0;). Some devices counter this and offset the Layout Viewport so that the focussed content remains in view.


Layout Viewport and Visual Viewport Behavior when the Virtual Keyboard gets shown (src).

Once once you start scrolling the page upwards again, the whole Layout Viewport will move position, and thus the item-at-the-bottom will be hidden underneath the keyboard again.

~

A Theoretical Solution

To deal with this you could use the Visual Viewport API and manually offset items once the Virtual Keyboard gets shown.

let pendingUpdate = false;

function viewportHandler(event) {
	if (pendingUpdate) return;
	pendingUpdate = true;

	requestAnimationFrame(() => {
		pendingUpdate = false;
		
		// Stick to top
		document.querySelector('[data-stickto="top"]').style.transform = `translateY(${ Math.max(0, window.visualViewport.offsetTop)}px)`;
		
		// Stick to bottom
		if (window.visualViewport.offsetTop >= 0) {
			document.querySelector('[data-stickto="bottom"]').style.transform = `translateY(-${Math.max(0, window.innerHeight - window.visualViewport.height - window.visualViewport.offsetTop)}px)`;
		}
	});
}

window.visualViewport.addEventListener("scroll", viewportHandler);
window.visualViewport.addEventListener("resize", viewportHandler);

Having played with it, I found this approach to be impractical and lacking:

  • Extra Math needed to deal with overscrolling
  • iOS: Only updates after scroll has finished
  • Android (Emulator): Scrolling becomes glitchy

☝️ Feel free to point out if I got my demo wrong there. Test it on your mobile device using this URL: https://codepen.io/bramus/debug/ExXxOLQ

~

A Proper Solution

As mentioned in The Large, Small, and Dynamic Viewports there’s a new kid in town that can help us out here: the VirtualKeyboard API.

With the API you can programmatically — e.g. via JavaScript — trigger the Virtual Keyboard and get its dimensions, but what I find more interesting is that it also provides some CSS environment variables:

  • keyboard-inset-top
  • keyboard-inset-right
  • keyboard-inset-bottom
  • keyboard-inset-left
  • keyboard-inset-width
  • keyboard-inset-height

The keyboard insets are six environment variables that define a rectangle by its top, right, bottom, and left insets from the edge of the viewport. Default value of the keyboard insets are “0px”

By default all these variables have a value of 0px. When the keyboard gets shown the value for keyboard-inset-height will change to reflect the height of the Virtual Keyboard. You can then use this as the bottom-margin on an element that’s anchored to the bottom of the Viewport:

.bottom-box {
  position: fixed;
  bottom: 0;
  margin-bottom: calc(20px + env(keyboard-inset-height));
}


In the new behavior the Visual Viewport will not shrink as the Virtual Keyboard gets shown, and you — as a developer — can take the keyboard dimensions into account to position items.

🤔 The word “Viewport” in that image indeed is confusing there, as it’s not specified which Viewport it is about. To make things clear: they’re talking but the Visual Viewport. I’ve filed an issue to clarify on this.

In a layout that uses CSS Grid, you can easily take this value into account to create a bottom-zone where nothing may be placed:

body {
    display: grid;
    height: 100vh;
    grid-template:
      "messages"  1fr
      "input"     auto
      "keyboard"  env(keyboard-inset-height, 0px);
}

The Grid Area named keyboard with either be 0px high or take up the size of the Virtual Keyboard, thus pushing the input Grid Area upwards as the keyboard gets shown.

~

The catch

To enable this new behavior, you need this little JS snippet:

if ("virtualKeyboard" in navigator) {
  navigator.virtualKeyboard.overlaysContent = true;
}

Personally I find this JS-requirement a shortcoming of this API. An extension to the meta name="viewport" (or perhaps a new CSS at-rule or property on <html>?) to control the behavior seems like something handier.

Something like this:

<!-- Proposal -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, virtual-keyboard=overlays-content">

Or this:

/* Proposal */
html {
  virtual-keyboard: overlays-content;
}

(Names to be bikeshed but you get the point)

💭 I’ve filed an issue in the spec repo regarding this.

~

Browser Support

The VirtualKeyboard API is shipping with Chromium 94. Other browser vendors have not signalled any interest in it yet. Relevant Issues:

~

More Info

There’s a good post up on web.dev that goes into more detail on the JavaScript interface I quickly mentioned. Be sure to give a read if you want to use the VirtualKeyboard API with your JavaScript-based apps. It also taps into the virtualkeyboardpolicy attribute you can use along with that.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

The Large, Small, and Dynamic Viewports

There are some changes being made regarding viewport units. The additions — which are part of the CSS Values and Units Level 4 specification — define several viewport sizes: the Large, Small, and Dynamic Viewport.

Thanks to these additions we will finally be able to solve that “100vh in Safari on iOS” issue.

~

Table of Contents

  1. The Large, Small, and Dynamic Viewports

  2. But, why? Can’t we just use the workaround?
  3. The catch
  4. Going Logical
  5. Browser Support
  6. In Closing

~

Update 2021.07.14: Some parts of this post have been rewritten to include the latest CSSWG changes regarding these units.

Update 2021.07.23: 🎉 The proposed changes have landed in the official spec by now. This post has been updated accordingly.

~

# The Large, Small, and Dynamic Viewports

The CSSWG has defined several extra Viewport Sizes and accompanying Viewport-relative Units (spec), in addition to the already existing vw/vh/vmin/vmax ones.

# The Large Viewport

The Large Viewport is the viewport sized assuming any UA interfaces (such as the address bar) that are dynamically expanded and retracted to be *retracted*. It has the l-prefix, so units are lvh / lvw / lvmin / lvmax.

For example: 100lvh stands for 100% of the large viewport height.

# The Small Viewport

The Small Viewport is the viewport sized assuming any UA interfaces that are dynamically expanded and retracted to be *expanded*. It has the s-prefix, so units are svh / svw / svmin / svmax.

For example: 100svh stands for 100% of the small viewport height.

# The Dynamic Viewport

The Dynamic Viewport is the viewport sized with *dynamic consideration of any UA interfaces*. It will automatically adjust itself in response to UA interface elements being shown or not: the value will be anything within the limits of 100lvh (maximum) and 100svh (minimum).

Its prefix is d, so the units are dvh / dvw / dvmin / dvmax.

👉 You’ll want these Dynamic Viewport Units to have a UI that auto-stretches as the UA interface changes. 100dvh will automatically adapt itself.

~

# But, why? Can’t we just use the workaround?

You might ask yourself why we can’t simply rely on this workaround instead of introducing all these new units:

body {
  height: 100vh;
}

@supports (-webkit-touch-callout: none) {
  body {
    height: -webkit-fill-available;
  }
}

Whilst the workaround above does achieve what we want, it only results in “full viewport height” for body. This is because -webkit-fill-available stretches items out inside their enclosing element — here the viewport. For some deeply nested elements that wouldn’t work, as -webkit-fill-available applied on them will look at their parent element — not the viewport. With 100dvh we can easily achieve what we want.

Above that you can’t use -webkit-fill-available to size something “half the size of the enclosing element”, as you can’t use -webkit-fill-available within calc(), e.g. height: calc(-webkit-fill-available * 0.5) is invalid CSS. Even if this were to be allowed one day, we’d again run into the issues when wanting to size a deeply nested element relatively to the viewport (see paragraph above).

So yes, we definitely do need these new Viewport Units 🤩

~

# The catch

As mentioned before these new proposed l*/s*/d* units are additions to the already existing vw/vh/vmin/vmax units. With this, the CSSWG chose to keep these “old” units ambiguous: vw/vh/vmin/vmax have no explicit definition, and it’s totally up to the UA (browser) to define how they behave. Some browsers will have vh behave like lvh (like the current Safari does), while other browsers can make vh behave like dvh.

What also is up to the UA to choose, is the behavior of the Dynamic Viewport. Some browsers may update its value immediately while the interface is changing, whereas other browsers may only update the value after the UI has transitioned … the spec is fine with both.

The UA is not required to animate the dynamic viewport-percentage units while expanding and retracting any relevant interfaces, and may instead calculate the units as if the relevant interface was fully expanded or retracted during the UI animation.

Above that things like on-screen keyboards are not taken into account. For that we have the upcoming Virtual Keyboard API.

~

# Going Logical

Additionally the spec now also defines logical units, and thus talks about vi/dvi/svi and vb/dvb/svb which are the inline and block size respectively of the large/dynamic/small viewport. A small but very welcome addition.

~

# Browser Support

At the time of writing no browser supports these units. You can track their progress in these Issues:

Update 2021.10.06: 👀 There’s some movement in the WebKit/Safari issue, with an initial patch being committed. Will we see this land as an experimental feature in one of the next Technology Preview releases?

Update 2021.11.15: 🛳 Support for these news units has shipped with Safari Technology Preview 135. Expect them to be publicly available in the next Safari release.

This pen below will indicate whether your browser supports CSS Dynamic Viewport Units or not:

See the Pen
CSS Dynamic Viewport Units test
by Bramus (@bramus)
on CodePen.

~

# In Closing

It feels great to see things finally move in this area I must say, as the reported WebKit bug about 100vh not being 100vh-as-we-expect-it-to-be dates back from 2015, and the relevant CSSWG Issue from 2019.

As a final consensus on these viewport additions has been reached, I hope that the upcoming Safari 15 — which alters the viewport extensively as you scroll up/down — will make work to include these additions on top of their already supported env(safe-area-inset-*) values (e.g. height: calc(100vh - env(safe-area-inset-bottom));).

~

To help spread the contents of this post, feel free to retweet the announcement tweet:

~

🔥 Like what you see? Want to stay in the loop? Here's how:

🗣 This post originally was a set of tweets.

100vh in Safari on iOS

When working with Viewport Units there’s this longstanding and extremely annoying bug in Safari on iOS where it does not play nice with the vh unit. Setting a container to 100vh for example will actually result in an element that’s a wee bit too tall: MobileSafari ignores parts of its UI when calculating 100vh.


Image by Max Schmitt

🤔 New to Viewport Units? Ahmad Shadeed has got you covered.

Apple/WebKit’s stance is that it works as intended, although it’s not what I (and many other developers) expect. As a result we have to rely on workarounds. In the past I’ve used Viewport Units Buggyfill or Louis Hoebregts’ CSS Custom Properties Hack to fix this behavior. I was glad to see that Matt Smith recently found a way to have MobileSafari render an element at 100vh using CSS:

As I replied on Twitter: Nice, but I’d rather have MobileSafari fix the vh unit, as using -webkit-fill-available for this will only work to achieving 100vh.

If you want to achieve a perfect 50vh for example, using -webkit-fill-available won’t work as you can’t use -webkit-fill-available in calc(). Above that it won’t work when the targeted element is nested somewhere deep in your DOM tree with one its parents already having a height set.

Come ‘on Safari, stop being the new IE6 …

UPDATE 2020.05.16 Apparently this -webkit-fill-available workaround can negatively impact the Chrome browser:

Given this it’s recommended to selectively ship -webkit-fill-available to only Safari using a @supports rule that tests for -webkit-touch-callout support:

body {
  height: 100vh;
}

@supports (-webkit-touch-callout: none) {
  body {
    height: -webkit-fill-available;
  }
}

Alternatively you can still use Louis Hoebregts’ CSS Custom Properties Hack, which uses JavaScript:

.my-element {
  height: 100vh;
  height: calc(var(--vh, 1vh) * 100);
}
const setVh = () => {
  const vh = window.innerHeight * 0.01;
  document.documentElement.style.setProperty('--vh', `${vh}px`);
};

window.addEventListener('load', setVh);
window.addEventListener('resize', setVh);

The ultimate guide to CSS Viewport Units

Ahmad Shadeed has done an extensive write-up on Viewport Units:

In this article, we will learn about CSS viewport units and how to use them, along with some use-cases and solutions for common issues. Let’s start and dig in!

It’s great to see that it’s an all-in-one article, covering use cases such as Viewport Unit Based Typography and Breaking Elements out of Their Containers.

Regarding Mobile Safari (iOS) not properly calculating 100vh the article mentions Louis Hoebregts’ solution which uses CSS Custom Properties to correctly set 1vh. In addition to that I can say I’ve had good results using Viewport Units Buggyfill in the past. A shame Apple still hasn’t fixed this issue.

CSS Viewport Units →

Rendering Sites Fullscreen in Safari on iPhone X / Introducing “User Agent Variables” (CSS Environment Variables)

What the …?

By default, the new iPhone X in landscape mode will contain sites in the so called “safe area”, resulting in white bars being rendered on either side of the site (src).

The color, white by default, can be tweaked by altering the background-color on the <body> element. Do note that it’s only background-color though: it doesn’t take gradients/background images into account, so you won’t jump very far with this …

Cover it up!

By adding viewport-fit=cover to the viewport meta tag, it’s possible to make the site stretch out beyond the safe area so that it takes up the full width of the device (src)

<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">

Embracing the notch

Whilst the use of viewport-fit=cover indeed stretches out the site, it also has a side effect of “the notch” overlapping with the site’s content.

To cater for this, Apple has proposed the concept of “User Agent Variables”, accessible via the constant() function in CSS.

This function has been renamed to env()

The current list of proposed User Agent Variables is:

  • user-font-size: User’s requested font size
  • user-background-color: User’s requested background color
  • user-foreground-color: User’s requested foreground color
  • safe-area-inset-top: Inset, as a <length> from the top of the viewport to the title-safe content area.
  • safe-area-inset-right: Inset, as a <length> from the right of the viewport to the title-safe content area.
  • safe-area-inset-left: Inset, as a <length> from the left of the viewport to the title-safe content area.
  • safe-area-inset-bottom: Inset, as a <length> from the bottom of the viewport to the title-safe content area.

Using these safe-area-inset-* constants as the padding of a site, overlap of the notch can be prevented.

body {
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

Additionally, these values can be used for elements that have position: absolute; / position: fixed; applied.

#element {
  position: fixed;

  /* default position */
  top: 0;

  /* “safe” position (iOS 11) */
  top: env(safe-area-inset-top);
}

Phew.

Did this help you out? Like what you see?
Consider donating.

I don’t run ads on my blog nor do I do this for profit. A donation however would always put a smile on my face though. Thanks!

☕️ Buy me a Coffee ($3)

Making viewport units work properly in Mobile Safari

A typical issue with the well supported Viewport Relative Units (you know: vh, vw, vmin, and vmax) that bothers me a lot is that MobileSafari (Safari on iOS) takes the height of the address bar into account for 100vh.

Take a look at the footer of that first block in the screenshot below: since its container exceeds 100% of the viewport’s height – even though said container is set to be 100vh in height – the date at the bottom bleeds out of the viewport:

viewport-units-buggyfill-without

Viewport Units Buggyfill is a script that fixes that kind of bad browser implementations. With Viewport Units Buggyfill applied, all is fine and dandy:

viewport-units-buggyfill-with

Next to initializing the script on load, on also needs to listen for the resize event in case – for example – the tabs bar get shown/hidden.

import * as viewportUnitsBuggyfill from 'viewport-units-buggyfill';

// …

// Initialize viewportUnitsBuggyfill
viewportUnitsBuggyfill.init();

// Also hook viewportUnitsBuggyfill to resize event (if it was initialized)
if (document.getElementById('patched-viewport')) {
    window.addEventListener('resize', viewportUnitsBuggyfill.refresh, true);
}

Viewport Units Buggyfill →

Did this help you out? Like what you see?
Consider donating.

I don’t run ads on my blog nor do I do this for profit. A donation however would always put a smile on my face though. Thanks!

☕️ Buy me a Coffee ($3)

Viewports Visualisation App

PPK:

Instead of the work I was supposed to do I spent about a day and a half on the alpha version of a viewports visualisation app.

It’s already been very useful to me, since figuring out how the viewports actually work is necessary for full understanding. I hope it does the same for you.

It also contains an example of absolute, fixed, and device-fixed position so that you understand the difference.

If you haven’t read up on the Layout Viewport and Visual Viewport, PPK also got you covered with his previously released “A Tale of Two Viewports”.

Viewports Visualisation App →
A Tale of Two Viewports →