How to keep header at top of visual viewport after layout/visual viewport changes in Chrome mobile web?
Asked Answered
J

5

9

I know there have been multiple SO questions about this, but none of them seem to work anymore since the browser teams have made changes since those questions were asked. I'm trying to find the most up-to-date solution to this problem.

Objective

My desire is to have a fixed header stay stationary at the top for an app-like experience when using a mobile web browser, specifically when the user has focus on an input field and the virtual keyboard is showing.

Background context

Recently, the Chrome mobile team changed how they resize their layout and visual viewports for mobile web to be in alignment with Chrome on iOS and mobile Safari on iOS.

This GIF from an article by David Fedor very succinctly demonstrates the current status of most (but not all) mobile browser with regards to the visual viewport being "moved up" when the On-Screen Keyboard (OSK, aka Virtual Keyboard) is shown when a field is focused.

The biggest issue I'm facing is that I cannot get a header element to stay visually present at the top when the OSK is shown. This is a big deal when your top appbar has primary actions for a form like "Save". It makes the UX confusing, which leads to customers leaving in frustration, so this is isn't something trivial in my opinion.

I've tried the VisualViewport API that Chrome team recommended, referencing the env(keyboard-inset-height) and that just doesn't seem to work, which makes me think I'm doing something weird with my layout such that the ENV isn't being set properly.

enter image description here

My code

Nothing too crazy, using CSS grid to control layout.

HTML

<html>
<body>
  <div id="container">
    <header id="header">header</header>
    <main id="main">
      main
      <input type="text" />
    </main>
    <footer id="footer">footer</footer>
  </div>
</body>
</html>

CSS style

body {
  margin: 0;
}

#container {
  height: 100dvh;
  display: grid;
  grid-template:
    "header" 50px
    "main" 1fr
    "footer" 50px
    "keyboard" env(keyboard-inset-height, 0px); // does not seem to work?
}

#header {
  grid-area: header;
}

#main {
  grid-area: main;
  overflow-y: scroll;
}

#footer {
  grid-area: footer;
}

What I get is the following 2 screenshots, the full-page one being the layout as I want it, and the 2nd one being when you focus the cursor into an input near the bottom and it "pushes" the entire visual viewport up such that the header is offscreen.

fullscreen input focused
enter image description here enter image description here

Is there any way currently to ensure that the header stays at the top of the visual viewport always? I cannot find a working solution to this at the moment after the recent Chrome updates.

Jeseniajesh answered 2/1, 2023 at 19:26 Comment(2)
I might come back and attempt to answer this question later. But if I forget, maybe this answer could be of help: https://mcmap.net/q/342541/-ios-15-safari-floating-address-bar It discusses the newer viewport dimensions, such as svh which may be handy. NOTE: These dimensions are much more widely available than the screenshot suggests, see caniuseBolero
Whoa, that's a nice animated example GIF! How'd you create that?Chemotropism
B
2

For Google Chrome 108 or later, set <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">.
Pay attention to interactive-widget property in the viewport meta tag.

I found the solution here.
Checked on the latest version of Chrome for Android.

body {
  margin: 0;
}

#container {
  height: 100dvh;
  display: grid;
  grid-template:
    "header" 50px
    "main" 1fr
    "footer" 50px
  ;
}

#header {
  grid-area: header;
}

#main {
  grid-area: main;
  overflow-y: scroll;
}

#footer {
  grid-area: footer;
}
<!DOCTYPE html>
<html>
<head>
  <title>Title</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
</head>
<body>
<div id="container">
  <header id="header">header</header>
  <main id="main">
    main 2<br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
    <input type="text" /><br>
  </main>
  <footer id="footer">footer</footer>
</div>
</body>
</html>
Bentonbentonite answered 3/8, 2023 at 14:36 Comment(1)
Not yet available on iOS Chrome, iOS Safari, or iOS Firefox, from my testing. Hopefully it will be implemented, but we'll need other workarounds in the meantime.Chemotropism
M
1

You can achieve this with window.visualViewport.height. This is the only value that is affected by the on-screen keyboard on iOS Safari. (try for yourself)

You'll have to set up a div that gets its size from that value, and an event listener on window.visualViewport?.addEventListener('resize', handleResize) to trigger an update whenever the visual viewport resizes. I also ran into a bug in iOS Safari where the new value was delayed by a frame after the resize event-- something to consider (see the link at the bottom for how I handled this).

Along with that you'll need to prevent scrolling of the window when an input element is selected. I believe there are multiple solutions for that, but I simply lock the body scroll down with this React hook;

const useOnScreenKeyboardScrollFix = () => {
  useEffect(() => {
    const handleScroll = () => {
      window.scrollTo(0, 0)
    }

    window.addEventListener('scroll', handleScroll)

    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [])
}

Finally, with for example flexbox, you can make any children of that div fixed to the top and bottom of the viewport like so:

iOS Safari fixed positioning with the on-screen keyboard

For more info and a ready-to-use React hook, see: https://martijnhols.nl/gists/how-to-get-document-height-ios-safari-osk

Marchese answered 6/4 at 16:30 Comment(0)
B
-2

CSS:

header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 100;
}

JS:

function debounce(func, delay) {
  let timer;
  return function () {
    clearTimeout(timer);
    timer = setTimeout(func, delay);
  };
}

// Attach debounced event listeners`enter code here`
window.addEventListener('scroll', debounce(onScroll, 50)); // Adjust the delay as needed
window.addEventListener('resize', debounce(onResize, 50)); // Adjust the delay as needed
Buxton answered 7/8, 2023 at 12:3 Comment(1)
You may want to edit your answer to explain why this resolves the asker's question, further guidance can be found in the Help CenterBolero
H
-3

Have you tried this?:

CSS

#header {
position: sticky;
}

If I understand your question correctly, you only care about the header remaining visible and not the document's body which contains the inputs. It seems like position: sticky; will give you this effect. If you don't want it to 'stick' unless an input is in focus, you could leverage some basic JavaScript to toggle its position value.

MDN docs give a decent explanation. Is this what you're looking to do? https://developer.mozilla.org/en-US/docs/Web/CSS/position

Hartzel answered 4/8, 2023 at 4:39 Comment(1)
Doesn't always work in iOS Safari. Both fixed and sticky items can get pushed out of the viewport when the keyboard is shown.Chemotropism
B
-3

#header, header { 
position: fixed; 
padding: 20px;
top: 0; 
z-index: 99999; 
} 
div.container{ 
background:#eee; 
display:block; 

}
body{ height:2000px;}
<body>
<header>
<div class="container"><h2>Sticky Header</h2></div>
</header>
</body>

also you can use position: absolute; and add media in css.

Bore answered 7/8, 2023 at 10:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.