We built a custom scroll-snap feature on our landing page - sections that smoothly glide into view when you scroll on desktop. It worked great. Then one day, users reported they were getting stuck and couldn't scroll past a certain section on desktop. Mobile was fine.
The culprit turned out to be a number that was 0.3 when it should have been 0.
What the Feature Does
On desktop, we use a custom wheel event handler to snap full-screen sections into view with a 2-second eased animation. On mobile, we skip all of this and let the browser's native scroll handle it.
The snap logic checks two conditions on every scroll event:
- › If approaching - snap the section fully into view.
- › If onGallery and scrolling down - snap forward to the next section.
Once both snaps are done, neither condition should be true, and normal scrolling resumes.
The Problem
After the second snap completed, users were completely stuck - no amount of scrolling moved the page. Every scroll event showed the same thing in the console:
Here's what was happening in order:
- › onGallery is true - so e.preventDefault() runs, blocking the browser's scroll.
- › The code tries to snap but finds the distance is only 0.3px - too small, so it exits without doing anything.
- › But the scroll was already blocked in step 1.
- › Since onGallery never becomes false, this repeats on every wheel event. The user is frozen.
The key question: why was onGallery still true after the snap was supposed to be done?
Why It Happened
onGallery is true when r.bottom > 0 - meaning the section is still at least partially visible. After snapping past the section, r.bottom should be exactly 0. On paper, the math works out:
But in practice, it was 0.09 - not zero.
Step 1: A text update introduced fractional pixel heights
Someone added a few lines of text to a bio section earlier on the page. That text rendered at:
Three extra lines added 72.09px (not 72px) to that section's height. Because this section sits above the gallery in the page, the gallery's vertical position became a fractional number like 2847.09px instead of a whole number.
Step 2: The browser rounded the scroll position
Our snap animation scrolled to 3647.09px. But some browsers (and Windows at 125% display scaling) round window.scrollY to the nearest integer, so it landed at 3647 - not 3647.09.
Step 3: The tiny leftover kept the condition alive
onGallery stayed true. The scroll stayed blocked. The user stayed stuck.
The Fix
Two options - we went with Option B for simplicity:
Option A - Check distance before blocking scroll
Only call e.preventDefault() after confirming the snap is actually needed:
Option B - Add a 2px tolerance (simpler)
Instead of comparing against exactly 0, allow up to 2px of wiggle room:
A 2px buffer is large enough to absorb any sub-pixel rounding, and small enough that it never accidentally ignores a real scroll trigger.
Key Takeaways
- › Don't block scroll before you're sure you need to. e.preventDefault() cancels the browser's scroll immediately - if your snap logic then decides there's nothing to do, the scroll is gone and the user is stuck.
- › CSS pixel values are not always whole numbers. getBoundingClientRect() returns sub-pixel values. font-size × line-height often produces fractions. Comparing these to exact zero is risky.
- › Browser scroll positions can be rounded. window.scrollTo(0, 3647.09) doesn't guarantee window.scrollY will equal 3647.09. On certain OS display-scaling settings, the browser rounds it - leaving a tiny leftover that can break exact comparisons.
- › Content changes can break unrelated logic. A longer paragraph elsewhere on the page changed layout offsets by a fraction, which silently invalidated JavaScript conditions in a completely different section.
- › Desktop-only code needs desktop testing. Because the snap logic is fully skipped on mobile, this bug was invisible on every touch device.