This article was originally posted on Medium in 2016. It may be out of date at this point.
(Sort of) Fixing autofocus in iOS Safari
One of my colleagues is transitioning to the front-end team that I used to be a part of. To prepare him mentally for his journey into front-end development, I’ve been sending him a newsletter I call Front-End Hack of the Day. I’m posting them to Medium now for the world to enjoy
Imagine that you are building a form where you would like to help the user out by automatically focusing on the first input field.
<form>
<input type="email" name="email" placeholder="foo@example.com" autofocus />
<input type="password" name="password" />
</form>
You fire it up and try it out, and it works great. Ship it!
Sometime later, someone comes to you and says that it’s not working in iOS Safari. Off you go to caniuse.com and see that it is indeed not supported in that browser. Oh well, no big deal, we can fix that with a little bit of Javascript.
document.addEventListener('DOMContentLoaded', () => {
Array.prototype.slice.call(document.querySelectorAll('input'))
.filter((el) => el.hasAttribute('autofocus'))[0]
.focus()
})
To your great surprise, you discover that this is not working either!
Turns out, Apple really doesn’t want you to focus input fields that the user hasn’t tapped on. Not only is the autofocus attribute not supported, but you have in fact made the situation worse!
See, even manually calling focus on the element won’t work until the user has interacted with the page. If the input is inside an iframe and you try to call focus before the user has interacted, the keyboard opens, the input does not get focus, and typing on the keyboard does absolutely nothing. As an added bonus, if the viewport scrolled at all, the useless, blinking cursor will be displayed somewhere outside the input.
I haven’t been able to find any official resource explaining this decision, but I have to assume that it’s because focusing a field pops up the keyboard, which can be annoying if you didn’t have any intention of filling out the field.
Faking focus
We can’t fully emulate the autofocus behavior, but we can get pretty close.
Focusing a field does three things:
- Set focus styles
- Scroll the page so the field is somewhere in the middle of the viewport
- Open the keyboard
3 is the only thing that Apple has something against, but the other two can be implemented rather easily. I’m going to show you a very specific example, but for your own sanity, I suggest you come up with ways to abstract over this so that you don’t need to worry about whether you’re really focusing the field or if you’re just faking it.
Check out @klarna/ui on Github to see how we are doing it (programmaticFocus.js).
The first part is simple, to set the focus styles, just add a class with the same styling:
input:focus,
input.has-focus {
border: green;
color: black:
}
Scrolling the input into view is surprisingly simple, thanks to Element.scrollIntoView.
If we put it all together, we get something like:
const isIos = () => !!window.navigator.userAgent.match(/iPad|iPhone/i)
const hasInteracted = (() => {
let interacted = false
const onTouchStart = {
interacted = true
document.removeEventListener(onTouchStart)
}
document.addEventListener('touchstart', 'onTouchStart')
return () => interacted
})()
const FOCUS_TYPES = {
REAL: 'real',
FAKE: 'fake'
}
const getFocusType = () => (hasInteracted() || !isIos())
? FOCUS_TYPES.REAL
: FOCUS_TYPES.FAKE
const focus = (input) => {
switch getFocusType() {
case FOCUS_TYPES.REAL:
return input.focus()
case FOCUS_TYPES.FAKE:
input.classList.add('has-focus')
const onBlur = (input) => {
input.classList.remove('has-focus')
document.removeEventListener(onBlur)
}
input.addEventListener('blur', onBlur)
input.scrollIntoView()
}
}
document.addEventListener('DOMContentLoaded', () => {
const autofocusedInput = Array.prototype.slice.call(
document.querySelectorAll('input')
).filter((el) => el.hasAttribute('autofocus'))[0]
focus(autofocusedInput)
})
What we end up with is a field that looks like it has focus and that is centered in the viewport. The keyboard won’t pop up, but that’s as close as we can get.
Hopefully this has been useful to you. The intention of these posts is not to show you some groundbreaking new front-end technique, but just to share some of the hacks my colleagues and I have had to implement over the years to deal with various browser quirks.
This was written mostly from memory, with some input from Xavier Via, so there may be some inaccuracies. Please leave a comment if I missed something.
Top comments (1)
When converting a NodeList to an Array, like you did here
I find it a little simpler to use
Array.from
Personal preference, and the same thing, but thought I would share.