Skip to content

Instantly share code, notes, and snippets.

@kiding
Last active May 17, 2024 20:14
Show Gist options
  • Save kiding/72721a0553fa93198ae2bb6eefaa3299 to your computer and use it in GitHub Desktop.
Save kiding/72721a0553fa93198ae2bb6eefaa3299 to your computer and use it in GitHub Desktop.
Preventing iOS Safari scrolling when focusing on input elements
<!--
When an input element gets focused, iOS Safari tries to put it in the center by scrolling (and zooming.)
Zooming can be easily disabled using a meta tag, but the scrolling hasn't been quite easy.
The main quirk (I think) is that iOS Safari changes viewport when scrolling; i.e., toolbars shrink.
Since the viewport _should_ change, it thinks the input _will_ move, so it _should_ scroll, always.
Even times when it doesn't need to scroll—the input is fixed, all we need is the keyboard—
the window always scrolls _up and down_ resulting in some janky animation.
However, iOS Safari doesn't scroll when the input **has opacity of 0 or is completely clipped.**
We can make use of this behavior. There are two possible workarounds, as shown below.
An upside to this approach beside being simple is that sniffing the User Agent string
to recognize iOS Safari might be no longer needed.
See it in action:
Before: https://twitter.com/kid1ng/status/1356167756622643200
After: https://twitter.com/kid1ng/status/1356166043169738762
https://twitter.com/kid1ng/status/1356176495991906305
See also:
Adobe's react-aria solves this problem in an incredibly spectacular way.
For more context and details, do take a look at the code and the comments at:
https://github.com/adobe/react-spectrum/blob/main/packages/@react-aria/overlays/src/usePreventScroll.ts
https://twitter.com/devongovett/status/1310652121063198720
https://twitter.com/jordwalke/status/1355681285717385217
-->
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>
#modal {
position: fixed;
background: gray;
top: 2em;
right: 2em;
left: 2em;
height: 10em;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#methodTwoWrapper {
position: relative;
}
</style>
<body>
<section>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque facilisis tellus pulvinar mauris lobortis, vel commodo erat finibus. Duis et ex eget nulla suscipit condimentum. Cras egestas arcu quis turpis congue vestibulum. Vestibulum et elit efficitur neque mollis semper. Vestibulum ut nisl dolor. Maecenas tempor nec tortor fringilla elementum. Sed eu porta orci. Proin mi enim, rhoncus quis pretium eu, posuere a ex. Mauris ac augue porta, laoreet urna nec, faucibus odio. Phasellus tempor sollicitudin velit a suscipit. Cras aliquam imperdiet ex vitae gravida. Curabitur hendrerit ante vel dolor maximus, at rhoncus mauris pulvinar. Etiam venenatis metus diam, at varius nisi elementum sit amet. Donec at rutrum odio. Integer vitae hendrerit orci, in molestie lacus. Ut cursus ipsum eu ultrices lacinia.</p>
<p>Nunc vitae varius neque, sit amet vehicula magna. Nunc lacinia, odio a aliquet rhoncus, metus turpis semper orci, eu tincidunt leo nisi id magna. Suspendisse quis dui velit. Pellentesque quis commodo mi. Suspendisse mauris dui, sagittis auctor orci vitae, laoreet hendrerit odio. Vivamus feugiat sapien erat, ornare tincidunt dui imperdiet a. Pellentesque accumsan quam ut consectetur ultrices. In porttitor nibh quis ligula fermentum efficitur. Maecenas a dui quis arcu fermentum porta nec a tellus. Integer accumsan finibus nisi. Nam suscipit sed risus nec mattis. Sed ut dui vel lectus vestibulum mattis.</p>
<p>Fusce iaculis pulvinar elit, in molestie leo efficitur a. Morbi vehicula lectus ac tortor fringilla sodales. Vestibulum commodo mollis euismod. Suspendisse gravida nisi mauris, nec varius nunc vulputate vel. Quisque vel tincidunt neque, nec rhoncus nisi. Sed et velit efficitur, porta purus in, accumsan urna. Etiam mauris odio, viverra accumsan convallis quis, convallis a odio. Nullam hendrerit massa turpis, et lobortis mi egestas eu.</p>
<p>Nulla ex justo, molestie id turpis non, porta luctus nibh. Nunc sollicitudin justo id velit maximus rhoncus. Vestibulum ut quam sit amet tellus eleifend dictum quis nec tellus. Donec faucibus, velit ac gravida lacinia, sem urna luctus sem, nec porttitor libero nisl et libero. Sed molestie ut nunc vitae tincidunt. Phasellus facilisis gravida odio non interdum. Nullam et varius dui, vel interdum orci. Proin sit amet ante lobortis, bibendum ex sodales, iaculis dui. Mauris tristique augue et elit sodales fermentum. Integer vulputate tellus arcu, ut maximus nunc imperdiet ac. Vestibulum imperdiet sem quis maximus euismod.</p>
</section>
<section id="modal">
<input placeholder="No workaround">
<input placeholder="Method 1" id="methodOne">
<div id="methodTwoWrapper">
<input placeholder="Method 2" id="methodTwo">
</div>
</section>
<script>
window.addEventListener("DOMContentLoaded", () => {
setTimeout(() => window.scrollTo(0, 100));
/*
* Method 1: Briefly change the opacity.
* Element might "blink" on focus in some scenarios.
*/
methodOne.addEventListener("focus", () => {
methodOne.style.opacity = 0;
setTimeout(() => methodOne.style.opacity = 1);
});
/*
* Method 2: Stack a cloned input element on top.
* Requires a relatively-positioned wrapper div.
* Might not be applicable to some scenarios.
*/
methodTwo.addEventListener("focus", () => {
const cloneTwo = methodTwo.cloneNode();
cloneTwo.removeAttribute("id");
cloneTwo.style.position = "absolute";
cloneTwo.style.top = 0;
cloneTwo.style.left = 0;
cloneTwo.style.opacity = 0;
methodTwoWrapper.append(cloneTwo);
cloneTwo.focus();
setTimeout(() => {
cloneTwo.style.opacity = 1;
methodTwo.style.opacity = 0;
methodTwo.disabled = true;
});
cloneTwo.addEventListener("blur", () => {
methodTwo.value = cloneTwo.value;
methodTwo.disabled = false;
methodTwo.style.opacity = 1;
cloneTwo.style.opacity = 0;
setTimeout(() => cloneTwo.remove());
});
});
});
</script>
</body>
</html>
@sonnd08
Copy link

sonnd08 commented Oct 19, 2022

Great, solution 1 work for me but I use CSS instead.

@keyframes blink_input_opacity_to_prevent_scrolling_when_focus {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

.input--focused {
    animation: blink_input_opacity_to_prevent_scrolling_when_focus 0.01s;
}

@dmitriev-dmitrii
Copy link

Great, solution 1 work for me but I use CSS instead.

@keyframes blink_input_opacity_to_prevent_scrolling_when_focus {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

.input--focused {
    animation: blink_input_opacity_to_prevent_scrolling_when_focus 0.01s;
}

i use this , thanks)

@Alexpeschel
Copy link

@sonnd08 🫰

@olivM
Copy link

olivM commented Mar 21, 2024

@sonnd08

🔥

@reshadman
Copy link

Working like a charm. fking around with GPT4 for hours and no working solutions. Curious to know how the hell did you first figured out the opacity workaround !!!

@Jaimaldullat
Copy link

Save my day... Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment