Blog

JS "click" event bubbling on iOS

iOS doesn't do the right thing, and how to fix it.

From Quirksmode way back in 2010:

From the dawn of history browsers have supported event delegation. If you click on an element, the event will bubble all the way up to the document in search of event handlers to execute.

It turns out that Safari on the iPhone does not support event delegation for click events, unless the click takes place on a link or input. That’s an annoying bug, but fortunately there’s a workaround available.

I forgot this bug even existed because FastClick fixes it while removing the 300ms delay that mobile browsers place on taps. Mobile browsers stopped implementing the tap delay (better UX) so FastClick isn’t needed anymore, which is good because it breaks <select> inputs in Chrome 53+ on Android (issue). Bye-bye FastClick.

And the iOS event delegation bug is back.

Raw workarounds

onclick

Adding an onclick event to non-clickable elements allows clicks to bubble fully. I don’t like this approach because any new element being inserted into the DOM requires extra code to support it.

cursor: pointer;

Adding cursor: pointer; to a non-clickable element’s CSS allows clicks to bubble fully. I don’t like this approach because you have to remember the reason this property is fixing a specific browser bug.

We could create a .clickable { cursor: pointer; } rule, but I’d rather use an approach that only affects one language (not CSS + HTML).

Nuclear approach with UA detection

With a little JS for user-agent detection we could apply the CSS fix to every element for iOS, which won’t matter because there is no cursor on iOS.

if (gravdept.isIos()) {
    document.body.classList.add('ios');
}
.ios * {
    cursor: pointer;
}

But UA detection and JS aren’t great dependencies to have, and again: two languages.

Best implementation

Creating a Sass mixin is my favorite approach because it encapsulates the code’s purpose, and is easily removable whenever iOS gets it right.

@mixin clickable {
    cursor: pointer;
}

div {
    @include clickable;
}

That’s how I’m handling this going forward.

Discourse Gravitated