32

I can only find questions where people have the opposite problem.

I want my fixed content to go above the iOS keyboard. Image of the problem:

fixed content goes below content on ios

I want iOS to behave like Android.

Is there a simple way to achieve this?

Parent element css:

.parent{
    position:fixed;
    top: 0;
    left 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
}

Button css:

.button{
    position:fixed;
    left 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 5rem;
}
Gautam Naik
  • 8,990
  • 3
  • 27
  • 42
Bjørnar Hagen
  • 757
  • 1
  • 8
  • 19

6 Answers6

20

We can use VisualViewport to calculate keyboard height. So we can set fixed-content pos correct.

Small demo: https://whatwg6.github.io/pos-above-keyboard/index.html

Code snippet:

const button = document.getElementById("button");
const input = document.getElementById("input");
const height = window.visualViewport.height;
const viewport = window.visualViewport;

window.addEventListener("scroll", () => input.blur());
window.visualViewport.addEventListener("resize", resizeHandler);

function resizeHandler() {
    if (!/iPhone|iPad|iPod/.test(window.navigator.userAgent)) {
      height = viewport.height;
    }
    button.style.bottom = `${height - viewport.height + 10}px`;
  }

function blurHandler() {
  button.style.bottom = "10px";
}
html,
body {
  margin: 0;
  padding: 0;
}

#button {
  position: fixed;
  width: 100%;
  bottom: 10px;
  background-color: rebeccapurple;
  line-height: 40px;
  text-align: center;
}
<input type="text" inputmode="decimal" value="0.99" id="input" onblur="blurHandler()" />
<div id="button">Button</div>

Problems: https://developers.google.com/web/updates/2017/09/visual-viewport-api#the_event_rate_is_slow

Why not innerHeight?: Iphone safari not resizing viewport on keyboard open

whatwg
  • 314
  • 2
  • 13
  • 1
    5 years later ... and there it is! This is great and seems to have good support, thank you! Also, nice color on that button :) – Bjørnar Hagen Aug 03 '22 at 22:33
  • repo: https://github.com/whatwg6/pos-above-keyboard/blob/main/index.html – whatwg Aug 05 '22 at 04:44
  • Don't know what's happening but this made the button appear too high up on my website but looks to be working fine in the demo – Glass Cannon Sep 08 '22 at 11:28
  • This answer is almost correct, but it won't work when scrolling all the way down. The number of pixels has to be: `window.innerHeight - viewport.height - viewport.offsetTop;`. Feel free to update your answer. – GermanJablo Aug 03 '23 at 12:15
11

Mobile Safari does not support position: fixed when an input focused and virtual keyboard displayed.

To force it work the same way as Mobile Chrome, you have to use position: absolute, height: 100% for the whole page or a container for your pseudo-fixed elements, intercept scroll, touchend, focus, and blur events.

The trick is to put the tapped input control to the bottom of screen before it activates focus. In that case iOS Safari always scrolls viewport predictably and window.innerHeight becomes exactly visible height.

Open https://avesus.github.io/docs/ios-keep-fixed-on-input-focus.html in Mobile Safari to see how it works.

Please avoid forms where you have several focusable elements because more tricks to fix position will be necessary, those were added just for demonstration purposes.

Note that for rotation and landscape mode, additional tricks are necessary. I'm working on a framework called Tuff.js which will provide a full-screen container helping mobile web developers to build web applications much faster. I've spend almost a year on the research.

By the way, to prevent scrolling of the whole window when virtual keyboard is active, you can use this super simple trick

var hack = document.getElementById('scroll-hack');

function addScrollPixel() {
  if (hack.scrollTop === 0) {
    // element is at the top of its scroll position, so scroll 1 pixel down
    hack.scrollTop = 1;
  }

  if (hack.scrollHeight - hack.scrollTop === hack.clientHeight) {
    // element is at the bottom of its scroll position, so scroll 1 pixel up
    hack.scrollTop -= 1;
  }
}

if (window.addEventListener) {
  // Avoid just launching a function on every scroll event as it could affect performance. 
  // You should add a "debounce" to limit how many times the function is fired
  hack.addEventListener('scroll', addScrollPixel, true);
} else if (window.attachEvent) {
  hack.attachEvent('scroll', addScrollPixel);
}
body {
  margin: 0 auto;
  padding: 10px;
  max-width: 800px;
}

h1>small {
  font-size: 50%;
}

.container {
  display: flex;
  align-items: top;
  justify-content: space-between;
}

.container>div {
  border: #000 1px solid;
  height: 200px;
  overflow: auto;
  width: 48%;
  -webkit-overflow-scrolling: touch;
}
<h1>iOS Scroll Hack</h1>
<p>Elements with overflow:scroll have a slightly irritating behaviour on iOS, where when the contents of the element are scrolled to the top or bottom and another scroll is attempted, the browser window is scrolled instead. I hacked up a fix using minimal,
  native JavaScript.</p>
<p>Both lists have standard scrolling CSS applied (<code>overflow: auto; -webkit-overflow-scrolling: touch;</code>), but the list on the right has the hack applied. You'll notice you can't trigger the browser to scroll whilst attempting to scroll the list
  on the right.</p>
<p>The only very slight drawback to this is the slight "jump" that occurs when at the top or bottom of the list in the hack.</p>
<div class='container'>
  <div id='scroll-orig'>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
      <li>10</li>
      <li>11</li>
      <li>12</li>
      <li>13</li>
      <li>14</li>
      <li>15</li>
      <li>16</li>
      <li>17</li>
      <li>18</li>
      <li>19</li>
      <li>20</li>
    </ul>
  </div>
  <div id='scroll-hack'>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
      <li>10</li>
      <li>11</li>
      <li>12</li>
      <li>13</li>
      <li>14</li>
      <li>15</li>
      <li>16</li>
      <li>17</li>
      <li>18</li>
      <li>19</li>
      <li>20</li>
    </ul>
  </div>
</div>

Got this answer from here

Gautam Naik
  • 8,990
  • 3
  • 27
  • 42
  • can you have a look at [my site](https://bejebeje.com) please? I have used `position: absolute;` but when the iOS keyboard pulls up, it still pushes everything up. – J86 Feb 26 '20 at 00:18
  • 10
    It's a shame that apple has not fixed this yet. it's 2021 – Iman Mohamadi Feb 24 '21 at 15:58
  • 5
    Apple is the new IE :( – Stephane Mar 17 '22 at 21:36
  • Accorading to https://stackoverflow.com/questions/39417778/iphone-safari-not-resizing-viewport-on-keyboard-open. window.innerHeight may not correct,so As of iOS 13 this appears to have been solved by using the VisualViewport API implementation.https://stackoverflow.com/a/59056851/7552246 – whatwg Mar 20 '22 at 13:20
  • +1 for the `addScrollPixel()` hack. The `Element.scrollHeight` [docs](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled) say the safest way to check if you've scrolled to the bottom is `Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) < 1` – Eric Mutta Jun 19 '22 at 21:39
  • I have the same fixed element issue when keyboard is displayed in Chrome on iPad, so this seems like an OS issue and not a browser issue. – thdoan Sep 29 '22 at 15:39
  • 1
    my god. deeply upsetting that such a basic usecase would require a year of research – Patrick Michaelsen Sep 30 '22 at 17:22
6

This is a well known problem, and unfortunately one must resort to hacky tricks like the accepted answer for now. The W3C is however in the process of specifying The VirtualKeyboard API.

Note: At the time of writing, this answer is not yet ready for prime time. It's important to understand that this specification must also be forward looking, to adapt to the myriad possible virtual keyboards of the future. It may be a few years before reliable cross platform browser support begins to appear and this answer becomes the correct one.

mseddon
  • 1,815
  • 17
  • 28
0

I found an interesting solution to this problem.

The solution is to create a hidden input and focus on it on the touchstart event.

<input id="backinput" style="position:absolute;top:0;opacity:0;pointer-events: none;">
<input id="input" style="position:absolute;bottom:0;">

Using JQuery:

$('#backinput').on('focus',function(e)
{
    e.preventDefault();
    e.stopPropagation();
    const input = document.getElementById('input');
    input.focus({ preventScroll: true });
})
$('#input').on("touchstart", function (event) {
    if(!$(this).is(":focus"))
    {
        event.stopPropagation();
        event.preventDefault();
        $('#backinput').focus();
    }
})

Finally, resize the viewport so that the bottom input moves above the keyboard (if needed)

window.visualViewport.addEventListener("resize", (event) => {
    $('body').height(parseInt(visualViewport.height));
});

For me it works perfect. I am building a messenger.

  • Setting `height` on `body` has no effect to me. Also tried on `html`, no effect. (I tried using jQuery on Safari, and using Chrome DevTools) – Nuno Jan 05 '23 at 19:24
0

There is changing the bottom css prop when the keyboard is shown or screen is scrolled. It is not what you want but it is the best that I could realize)

Below you can see my React hook and its usage.

import _ from 'lodash'
import { useRef, useEffect } from 'react'

export const useFixedPositionWithOpenedIOSKeyboard = (extraBottomOffset = 10) => {
  const elementRef = useRef(null)

  useEffect(() => {
    if (/iPhone|iPad|iPod/.test(window.navigator.userAgent)) {
      const setElementOffsetBottom = () => {
        const screenHeight = document.documentElement.clientHeight
        const screenHeightWithoutKeyboard = visualViewport?.height ?? 0
        const offsetTop = visualViewport?.offsetTop ?? 0

        if (elementRef?.current) {
          const elementStyles = (elementRef.current as HTMLElement).style

          if (Math.round(screenHeightWithoutKeyboard) < screenHeight) {
            elementStyles.bottom = `${
              screenHeight - screenHeightWithoutKeyboard - offsetTop + extraBottomOffset
            }px`
          } else {
            elementStyles.bottom = ''
          }
        }
      }

      const debounceElementOffsetBottom = _.debounce(setElementOffsetBottom, 150)
      const showElement = () => debounceElementOffsetBottom()

      window.addEventListener('scroll', showElement)

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

  return elementRef
}

...

export const Component = () => {
  const buttonRef = useFixedPositionWithOpenedIOSKeyboard();

  return (
    <>
      <input type='text' />
      <button type='submit' ref={button} style='
        position: fixed;
        bottom: 40px;
        left: 50%;
        transform: translate(-50%);
        transition: bottom 0.5s cubic-bezier(0.4, 0, 0.2, 1);
      '>
        This is Button
      </button>
    </>
  );
}
Adriaan
  • 17,741
  • 7
  • 42
  • 75
WFZ
  • 11
  • 2
0

I adapted the whatwg's answer because it didn't work for my website (I don't know why exacly). I use the property top instead of bottom for the absolute div. My page is a chat, my div contains an input.

Here my solution:

// Only for Safari on iOS
// (use interactive-widget=resizes-content to fix Chrome)
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
    if (navigator.userAgent.indexOf('Chrome') === -1 && navigator.userAgent.indexOf('Safari') > -1) {

        // Put the body relative
        document.body.style.position = 'relative';
        let marginTop = parseInt(window.getComputedStyle(document.body).marginTop);
    
        // My toolbar (in my case, a div with an input inside to make a chat)
        myBottomDiv.style.position = 'absolute';
    
        // Events (touchmove on mobile, because the scroll event doesn't work well)
        window.addEventListener("scroll", resizeHandler);
        window.addEventListener("touchmove", resizeHandler);
        window.visualViewport.addEventListener("resize", resizeHandler);
    
        function resizeHandler() {
            myBottomDiv.style.top = (window.scrollY +  window.visualViewport.height - marginTop - myBottomDiv.offsetHeight) + 'px';
        }
    }
}
Coudrak
  • 111
  • 1
  • 4