← More gists
Published

How to get the document height in iOS Safari when the on-screen keyboard is open

When it comes to the on-screen keyboard (OSK), Safari on iOS behaves weirdly. The normal ways of getting the screen size do not work as expected. When the keyboard is opened in Safari, it moves part of the viewport out of sight instead of simply resizing it. This leads to getting the old values (i.e. the screen size), despite the keyboard taking a large chunk of the screen.

We can get the real viewport size using the in 2019 introduced window.visualViewport property. This is the only value I found that accounts for the on-screen keyboard.

This is an essential part that allows us to make layouts with a fixed header and footer, even when the keyboard is open or changes sizes. An example of this in one of my apps is shown in the GIF below.

To achieve this, I set up a div with the height from the useViewportSize hook below. I also needed to prevent Safari from scrolling when the input is selected, for that I used a separate fix.

import { useCallback, useEffect, useState, useLayoutEffect } from 'react'
const useBrowserLayoutEffect =
typeof window !== 'undefined'
? useLayoutEffect
: // eslint-disable-next-line @typescript-eslint/no-empty-function
() => {}
type Width = number
type Height = number
type Size = [Width, Height]
/**
* Get the current size of the Viewport. Do not call this excessively, as it may
* cause performance issues in WebKit. Querying innerWidth/height triggers a
* relayout of the page.
*/
export const getViewportSize = (): Size => {
if (window.visualViewport) {
// visualViewport is a new prop intended for this exact behavior, prefer it
// over all else when available
// https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API
return [window.visualViewport.width, window.visualViewport.height] as const
}
return [
window.innerWidth,
// window.innerHeight gets updated when a user opens the soft keyboard, so
// it should be preferred over documentElement.clientHeight
// Want more? https://blog.opendigerati.com/the-eccentric-ways-of-ios-safari-with-the-keyboard-b5aa3f34228d
window.innerHeight,
] as const
}
/**
* Returns the viewport size. This can also be used as a dependency in a
* useEffect to trigger an update when the browser resizes.
*/
const useViewportSize = () => {
const [viewportSize, setViewportSize] = useState<Size | undefined>()
const updateViewportSize = useCallback(() => {
const viewportSize = getViewportSize()
setViewportSize((oldViewportSize) => {
if (
oldViewportSize &&
oldViewportSize[0] === viewportSize[0] &&
oldViewportSize[1] === viewportSize[1]
) {
// Maintain old instance to prevent unnecessary updates
return oldViewportSize
}
return viewportSize
})
}, [])
useBrowserLayoutEffect(updateViewportSize, [updateViewportSize])
useEffect(() => {
const effectTwice = () => {
updateViewportSize()
// Closing the OSK in iOS does not immediately update the visual viewport
// size :<
setTimeout(updateViewportSize, 1000)
}
window.addEventListener('resize', effectTwice)
// From the top of my head this used to be required for older browsers since
// this didn't trigger a resize event. Keeping it in to be safe.
window.addEventListener('orientationchange', effectTwice)
// This is needed on iOS to resize the viewport when the Virtual/OnScreen
// Keyboard opens. This does not trigger any other event, or the standard
// resize event.
window.visualViewport?.addEventListener('resize', effectTwice)
return () => {
window.removeEventListener('resize', effectTwice)
window.removeEventListener('orientationchange', effectTwice)
window.visualViewport?.removeEventListener('resize', effectTwice)
}
}, [updateViewportSize])
return viewportSize
}
export default useViewportSize

While this is made to be compatible with Server Side Rendering / Static Site Generation, it is not possible to get the size of a client's screen on the server-side. This will always return 0 on the server.

I hope this helps. If you need help, let me know on Twitter.

Update 2024-04-04: After some testing I found that unlike the window resize event, window.visualViewport's resize event is also triggered for the scrollbar appearing and disappearing in regular browsers. That might come in handy.