Mobile long-press UX: making “hold” interactions feel right

2025-12-19~4 min read

Problem: long-press shows text selection UI

  • Implementing “hold” on a button can trigger selection/callout menus on mobile
    • You want hold to control the game, but the OS/browser shows selection UI
    • This interrupts gameplay and feels broken

Fix: `user-select: none`

  • Apply plain CSS user-select: none to controls
    • Prevents long-press selection menus from appearing
    • Lets players focus on the game controls

Implementation Example

When using Tailwind CSS, add the select-none class.

// IconButton component example
export function IconButton({ children, ...props }: IconButtonProps) {
  return (
    <button
      {...props}
      className={`
        bg-black/50 hover:bg-black/70
        text-white font-bold
        transition-all
        select-none  // ← Prevents selection menu on long-press
        ${className}
      `}
    >
      {children}
    </button>
  );
}

When using plain CSS:

.game-button {
  user-select: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
}

Long-press Implementation Example

When implementing long-press, use onPointerDown and onPointerUp.

export function HoldIconButton({ onHoldStart, onHoldEnd, children }: Props) {
  return (
    <IconButton
      onPointerDown={(e) => {
        e.currentTarget.setPointerCapture(e.pointerId);
        onHoldStart();
      }}
      onPointerUp={onHoldEnd}
      onPointerCancel={onHoldEnd}
      className="select-none"  // Prevents selection on long-press
    >
      {children}
    </IconButton>
  );
}