I'm not a developer. I'm a sales guy who started messing around with AI-assisted coding and, a few months later, had somehow built a bunch of browser games and a website. It mostly goes fine. Then occasionally something breaks in a way that makes clear I have no idea what I'm doing, and I spend way longer on it than I should.
This is one of those.
The bug
The games affected were Capital Punishment (a world capitals geography quiz) and Math Blitz (a speed math game). Both have the same core interaction: you see a question, you tap one of four answer buttons, the result flashes, and the next question loads.
On iOS Safari, something wrong was happening in that last step. When the next question loaded, one of the new answer buttons would appear highlighted. Slightly pressed-looking, visually active. Always the button in the exact same position as the one you had just tapped. Tap the top-left button, and on the next question, the top-left button looks like it's being pressed even though you're not touching it.
It sounds minor. It looked terrible. And it only happened on iPhone.
My first thought was: this is a CSS problem. I know just enough about how browsers style things to have an opinion here, and the highlight looked like what happens when a button gets stuck in its "active" state. That is the visual style buttons show only while you're pressing them.
Everything I tried that didn't work
I went after the CSS first. In web development, buttons have different visual states: default, hovered, active (while being pressed), and disabled. The active state on my buttons had a subtle scale effect. They'd shrink very slightly when tapped, like a physical button depressing. I figured iOS was getting confused and applying this style to the wrong button.
So I removed it. Stripped out the active state styling entirely. The bug didn't care. The highlight persisted on the wrong button regardless.
Then I went after the timing. When you tap an answer, my code hides the button grid, processes your answer, builds a new set of buttons for the next question, and then shows the grid again. I was doing this inside something called a requestAnimationFrame callback, which is a browser tool that lets you say "do this on the next available screen redraw." The idea was to give the browser a moment to breathe between the old buttons disappearing and the new ones appearing.
The theory was that iOS needed more time to release whatever internal state was causing the highlight. So I tried waiting two frames instead of one โ a technique called double requestAnimationFrame, where you nest one callback inside another. That buys you roughly 32 milliseconds instead of 16. Still broken.
At this point I started getting creative in ways that did not reflect well on me. I added a global listener that fired blur(), which tells the browser to unfocus whatever element it thinks is active, every single time a finger left the screen anywhere on the page. I blocked all touch input on the entire game container for two frames after every answer. I tried adding CSS properties specifically designed to tell iOS Safari to stop doing whatever it was doing with touch states.
I documented every failed attempt in a handoff file for my next AI session. It got long. Nothing worked.
What was actually happening
Here is the thing about onclick that I did not know. It is the standard way to make a button respond to a tap, but on iOS Safari it does not behave the way you'd expect.
On iOS Safari, onclick fires while your finger is still touching the screen, not when you lift it. The browser waits to confirm you're not doing a scroll or a pinch or something else, and once it decides this was a tap, it fires the click event. But at that moment, your finger is still physically there.
So here is what was happening in sequence: I tap a button. iOS decides this is a tap and fires onclick. My code runs, hides the old buttons, and loads a new set of buttons. The new buttons appear. My finger is still touching the screen in exactly the position where I tapped. iOS looks at that touch point, sees a button sitting right there, and applies the active state to it.
I wasn't dealing with a stuck state from the previous button. I was dealing with a live touch state being applied to a brand new button that had materialized under my finger. Every fix I tried was addressing what I thought was a lingering problem. The actual problem was that the touch was still ongoing.
The fix
The solution was switching from onclick to ontouchstart.
Where onclick fires during the touch, touchstart fires the instant the finger lands. This means by the time my code runs, processes the answer, and loads the next question, the player has already lifted their finger. The touch is over. When the new buttons appear, there's nothing pressing them.
let touchFired = false;
btn.addEventListener('touchstart', (e) => {
e.preventDefault();
touchFired = true;
handleAnswer(choice, btn, choices, answer, grid);
}, { passive: false });
btn.addEventListener('click', () => {
if (!touchFired) handleAnswer(choice, btn, choices, answer, grid);
touchFired = false;
});
The e.preventDefault() line tells iOS not to fire the synthetic click event it generates a few hundred milliseconds after a tap. Without it, the answer would register twice. The touchFired flag covers older iOS versions that handle this slightly differently. And the regular click listener stays in place so everything still works normally on desktop with a mouse.
That was it. Weeks of sessions, a multi-page handoff document, and a growing sense that I fundamentally did not understand how touch screens worked. Fixed by changing one word and adding about eight lines of code.
In retrospect
If you're building anything interactive for mobile, onclick and touch events are not interchangeable. iOS Safari in particular handles them differently than every other browser. Worth knowing before you need it.
The bigger thing I took from this is that I kept fixing the wrong thing. The button looks highlighted, so I went after the highlight. It never occurred to me to ask why the browser thought something was being pressed in the first place. Once I understood the touch was still active when the new buttons appeared, the answer was obvious. Getting there took way longer than it should have.
The games work on iPhone now. Took a while. Moving on.