Making tooltips work on touchscreen

In my previous post, I took extra care to make tooltips more accessible. But the whole conversation is still centered around devices that have hover capabilities (and then some assistive technology, like screen readers). What about mobile phones and other touchscreen devices?

One of my suggestions in that article was to guard hover events under a hover media query. This does improve the experience by avoiding accidentally triggering tooltips on some mobile browsers, but the drawback is that our tooltips are now almost completely inaccessible to mobile users.

This is why it helps to think of tooltips as completely optional enhancements. If an interface requires tooltips to be usable, then it might be time to rethink the design.

However, they are still an enhancement, so wouldn’t it be nice if we could make this enhancement available to touch users?

As an Android user, I’m used to icon buttons within native apps displaying tooltips on long press. This often helps me learn what the button does if I’ve never seen it before. The tooltip is displayed as long as I’m pressing the button, and when I let go, it disappears after a short delay. When the tooltip appears, I also get some haptic feedback, which I thought was a nice touch (ha!) to indicate that my long press was registered.

Let’s see if we can recreate the same experience on the web.

Here be dragons

Before we start, I just want to emphasize that this technique is untested and I have not seen it used in any website out in the wild. Treat this as an experiment, rather than advice.

Generally, mobile browsers already reserve long press for things like opening a context menu (e.g. on a link or image) or selecting text or showing clipboard options. Buttons do not have such default actions reserved (button text is usually unselectable, and icon buttons don’t even have text); this makes buttons a good candidate to test this technique on. For other elements (like links and inputs), it might be more approriate to use a different event to trigger the tooltip, or better yet, just avoid using tooltips entirely and always show the text.

Detecting long presses

There is no “longpress” event on the web, so we need to write some custom code.

Assuming that we already have a reference to the trigger element (similar to my Tooltip class from my last post), we can start adding event listeners.

First up, we’ll use pointerType to exclude mouse/pen devices. Then we’ll store the isTapping state set to true in ‘pointerdown’ and reset it in ‘pointerup’. Essentially, the value will be true for as long as the trigger is being tapped. We’ll also add the same handler as ‘pointerup’ for ‘pointerleave’, because the long press might start on the trigger but can be moved out out of the trigger before releasing.

let isTapping = false;
function pointerdown(e) {
if (e.pointerType !== 'touch') return;
isTapping = true;
function pointerup(e) {
if (e.pointerType !== 'touch') return;
isTapping = false;
trigger.addEventListener('pointerdown', pointerdown);
trigger.addEventListener('pointerup', pointerup);
trigger.addEventListener('pointerleave', pointerup);

Now to detect the actual long press, we just need to wait 500 ms and check the isTapping state again. If isTapping is no longer true, it means the finger was released before the setTimeout completed. Otherwise, it is indeed a long press!

async function pointerdown(e) {
await new Promise((resolve) => setTimeout(resolve, 500));
if (!isTapping) return;
// this is a long press now!

You can test out the long press logic on your mobile device yourself:

Displaying the tooltip

Now that we have detected a long press, we are ready to trigger our tooltip. If you followed my previous article, there is already a show() function ready to call. (If not, you can substitute this part with your own show function).

Let’s also add that sweet haptic feedback using navigator.vibrate() (Chrome Android only).

Also also, because we only want this behavior for buttons, we can early return if the trigger is not a button.

async function pointerdown(e) {
if (e.currentTarget.nodeName !== 'BUTTON') return;

Finally, let’s hide the tooltip after a short delay once the pointer is released.

async function pointerup(e) {
await new Promise((resolve) => setTimeout(resolve, 1500));

And that’s all there is to it!

Live demo