Kristóf PoduszlóKristóf Poduszló
  • GitHub
  • Bluesky
  • LinkedIn

Half a pixel from the bottom

3 min readSep 13, 2026

Your chat looks caught up, yet new messages land out of view. The browser thinks you scrolled away, and it’s not wrong.

Reread a conversation while replies keep arriving below, and the message you’re reading stays in place. That’s scroll anchoring, enabled by default through overflow-anchor. Safari 27 brings it to every major browser.

When you return to the bottom, a chat should follow new messages instead. Here’s Matthias Zöchling’s bottom-anchored scrolling area, which builds on Kitty Giraudel’s:

<div class="chat">
<!-- Keeps messages in order -->
<div>
<!-- messages, oldest first -->
</div>
</div>
.chat {
display: flex;
flex-direction: column-reverse;
overflow-y: auto;
}

column-reverse moves the scroll origin to the bottom. That means scrollTop stays at 0 until you scroll up, and goes negative from there.

If a scroll misses 0, even slightly, CSS alone no longer follows new messages:

warning:Your browser has no scroll anchoring, so both chats below behave the same.
Demo
Unguarded

CSS only

  1. Message 1
  2. Message 2
  3. Message 3
  4. Message 4
  5. Message 5

Guarded by rounding
  1. Message 1
  2. Message 2
  3. Message 3
  4. Message 4
  5. Message 5

  1. Click Scroll near bottom. Both chats stop less than a pixel above the bottom, but the guarded one snaps back to 0. The demo renders each chat at 2×, so its scroll position can land on a half pixel.
  2. Click Add message a few times. Only the guarded chat follows.
  3. Repeat both steps with floor, then round. Whether round fails depends on the pixel density of your screen.

Only zero counts as the bottom

When the scroll position is exactly 0, the browser suspends scroll anchoring. At any other value, it assumes you scrolled away on purpose and holds your place.

Scroll positions snap to whole device pixels. On a 2× high-DPI display, one device pixel is half a CSS pixel, so a scroll can stop at -0.5px.

The latest message still looks fully in view, yet the browser holds it in place. Each new reply lands out of view.

warning:

Scrolling isn’t the only cause. Fractional message heights can leave the scroll position slightly off 0.

Snap back to zero

overflow-anchor: none makes the chat follow again, but then nothing holds your place while you reread. Instead, snap any position within a pixel of 0 to the bottom:

chat.addEventListener("scroll", () => {
if (Math.trunc(chat.scrollTop) === 0) {
chat.scrollTop = 0;
}
});

Math.trunc drops the fraction, and the -0 it returns equals 0. That turns the guard into a range check, the same as Math.abs(chat.scrollTop) < 1. An overscroll bounce goes further than a pixel, so the condition ignores it.

Other rounding functions miss part of that range:

  • Math.floor rounds every negative fraction down to -1.
  • Math.round works at 2×, where -0.5 rounds to -0. But at 3×, -0.667 rounds to -1.

Wait for scrollend

The guard runs on every scroll event. A gesture slower than a pixel per frame never moves the chat away from 0, because each event snaps it back. Listen for scrollend instead. It fires after you finish scrolling, so the condition checks only where you stop. Keep scroll as a fallback for browsers released before December 2025.

Scroll up to reread, and your place holds. Stop half a pixel from the bottom, and the next reply still lands in view.

Share this post with someone who’d like it. You can also sponsor my work.

  • Previous post:← I’m gay, and so are you
  • Email me
  • RSS feed
Made with ♥ in Budapest
© 2023–2026 Kristóf Poduszló