You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[cssom-view] Clarify number and order of scroll and scrollend events when a scroll event listener performs another scroll #13772
Safari prints scroll, scroll, scrollend
Chrome prints scroll, scrollend, scroll, scrollend
Firefox prints scroll, scrollend, scroll
Firefox's behaviour is clearly undesirable (the last event fired should be scrollend), but there is also behaviour divergence between Chrome and Safari.
I'm trying to understand what the spec says should happen in this scenario, but my naive reading of it suggests that even the number of scroll events fired should be just one (which is a behaviour no browser has)!
When the first scroll scrollTop = 100 happens, (doc, "scroll") is added to the document's pending scroll events list.
When the event loop gets to "run the scroll steps", the list of pending scroll events is iterated over. At this point it only contains the (doc, "scroll") entry mentioned above, so it's processed.
At the time of processing, the entry is not yet removed from the list. (The step "empty doc's pending scroll events" only happens at the end of the loop.)
Processing the entry involves firing a scroll event at the document, which will run the event listener which performs scrollTop = 200.
When running the event listener and performing the scroll, the following clause is reached: "If (doc, "scroll") is already in doc’s pending scroll events, abort these steps.". Since the first entry has not yet been removed from the list, a second entry is never added, and so no second scroll event is fired.
If my interpretation is incorrect, please let me know where I went wrong; otherwise, I think we need to make some clarifications/edits here.
It matches my understanding. I would say that the spec does just need to add some words something like "move the pending scroll events into a temporal list" and process each item in the list... etc. With that any new scroll events will be queued in the pending scroll events queue, not in the one which is currently being processed.
I would say that the spec does just need to add some words something like "move the pending scroll events into a temporal list" and process each item in the list... etc. With that any new scroll events will be queued in the pending scroll events queue, not in the one which is currently being processed.
Do you have in mind the pending scroll events queue then being processed again within the same refresh driver tick? Or would a scroll event queued during the processing of a scroll event listener have to wait until the next refresh driver tick to be processed?
I would say that the spec does just need to add some words something like "move the pending scroll events into a temporal list" and process each item in the list... etc. With that any new scroll events will be queued in the pending scroll events queue, not in the one which is currently being processed.
Do you have in mind the pending scroll events queue then being processed again within the same refresh driver tick? Or would a scroll events queued during the processing of a scroll event listener have to wait until the next refresh driver tick to be processed?
The latter, just like nested requestAnimationFrames.
The latter, just like nested requestAnimationFrames.
Ok. With this change, if I'm understanding correctly, the resulting behaviour on the testcase will be the Chrome one, i.e. the dispatched events are scroll, scrollend, scroll, scrollend.
@smfr@bramus Do you have any thoughts on the clarification discussed above and the resulting behaviour of the testcase in the issue description?
Personally, I think the way I'd fix this would be:
The scroll event queue should be snapshotted before dispatching scroll events, just like the requestAnimationFrame behavior. This alone gets the Chromium behavior.
Additionally, we could add a check when dispatching the scrollend event like "if there's a pending scroll event for this node, return", to get the Safari behavior.
I don't feel too strongly about the Safari vs. Chromium behavior, tho. We can choose which one we want by adding or removing (2).
The way this works in chrome is all of the events (i.e. scroll and scrollend) are already enqueued before we start dispatching. This is why even if you start scrolling in response it doesn't prevent the scrollend from being dispatched - since it technically ended before you received the scroll event. IMO this is expected since per spec scroll events are added to a queue and dispatched later.
FWIW, if the initial scrollTop operation in the test case is done in a requestAnimationFrame callback, such as requestAnimationFrame(() => { document.scrollingElement.scrollTop = 100; });
Safari prints scroll, scroll, scrollend, scroll, scrollend
So it looks to me that Safari kinda agrees on the Chrome's behavior.
Note that I tested it on iOS 26.4.1.
(EDITED: I originally mis-typed scrollTop = 200, I am sorry for the confusion)
The CSS Working Group just discussed [cssom-view] Clarify number and order of `scroll` and `scrollend` events when a `scroll` event listener performs another scroll, and agreed to the following:
RESOLVED: snapshot the event queue and empty it before dispatching all the events
The full IRC log of that discussion
<fantasai> scribe+
<emilio> flackr: this is about what events you should expect to see when you perform a scroll from a scroll event listener
<emilio> ... I think the expectation is that because scroll events are dispatched after the scroll finished you'll see the queued scrollend from the previous scroll
<emilio> q+
<astearns> ack emilio
<fantasai> emilio: I think that falls out of the current spec, but bit that is not specced is that you copy the queued list of scroll events before dispatching all of them
<fantasai> emilio: We have this scroll event queue
<fantasai> emilio: I think that's the behavior you want?
<fantasai> flackr: Yes, I think so
<fantasai> emilio: So that needs clarification
<fantasai> emilio: Otherwise conceptually you could end up with an almost synchronous scroll event
<fantasai> flackr: completely agree. Copy beforehand.
<fantasai> emilio: That matches rAF etc.
<fantasai> astearns: Does that fit the behavior you thought was correct smfr?
<fantasai> smfr: I think it does result in scroll/scrollEnd/scroll/scrollEnd pattern
<fantasai> smfr: Idk whether Safari behavior is better.
<fantasai> smfr: Would like to hear from authors whether having a scrollEnd in the middle is problematic
<emilio> flackr: keep in mind that new scroll would end up in the next update the rendering
<fantasai> smfr: So split into different frames... maybe that's ok
<ydaniv> I think it's ok
<fantasai> emilio: Proposal is to snapshot the event queue and empty it before dispatching all the events
<flackr> +1
<emilio> PROPOSED: snapshot the event queue and empty it before dispatching all the events
<emilio> RESOLVED: snapshot the event queue and empty it before dispatching all the events
<emilio> astearns: does this cover your second point of checking whether there's a pending scroll event?
<emilio> flackr: I think this covers it
Are any of the browser vendors making any progress on this?
Because I suspect that universally, applications which need to clear a state or take some other action when a scroll is finished use a timer which kicks off on every scroll event, cancelling the last timer from the last scroll event if any.
This is incredibly wasteful.
But the scrollend event MUST actually work, and fire when a scroll has completely finished for this pattern to stop being used. Otherwise some states will be locked in place.
Even when they all pull their fingers out (And Safari probably will not for another few years), after all browsers have fixed this, event even then we here will not be able to roll out a better, more efficient way of responding to scrollend for another two years after that (Our legacy browser support policy allows use of browsers up to 2 years old)
I might be retired before that happens. I will email my old colleagues to remind them about this issue!
@ExtAnimal From Chromium’s side there is https://issues.chromium.org/issues/510319892 that you can track. Hit the +1 in the top right to signal that you are impacted by this, or hit the star (next to the title of the bug) to subscribe to updates. The bug is currently assigned, which is already quite a good thing :)
Activity
theres-waldo commentedon Apr 6, 2026
cc @hiikezoe
hiikezoe commentedon Apr 6, 2026
It matches my understanding. I would say that the spec does just need to add some words something like "move the pending scroll events into a temporal list" and process each item in the list... etc. With that any new scroll events will be queued in the pending scroll events queue, not in the one which is currently being processed.
theres-waldo commentedon Apr 8, 2026
Do you have in mind the pending scroll events queue then being processed again within the same refresh driver tick? Or would a
scrollevent queued during the processing of ascrollevent listener have to wait until the next refresh driver tick to be processed?hiikezoe commentedon Apr 9, 2026
The latter, just like nested requestAnimationFrames.
theres-waldo commentedon Apr 9, 2026
Ok. With this change, if I'm understanding correctly, the resulting behaviour on the testcase will be the Chrome one, i.e. the dispatched events are
scroll,scrollend,scroll,scrollend.@smfr @bramus Do you have any thoughts on the clarification discussed above and the resulting behaviour of the testcase in the issue description?
smfr commentedon Apr 9, 2026
To me Safari's behavior makes more sense. It seems wrong to issue a
scrollendwhen another scroll has been issued.bramus commentedon Apr 10, 2026
Deferring to @DavMila and @flackr for this.
theres-waldo commentedon Apr 11, 2026
@emilio Would it be possible to get this issue on the agenda of an upcoming meeting?
emilio commentedon Apr 11, 2026
@theres-waldo Added to the agenda :)
emilio commentedon Apr 11, 2026
Personally, I think the way I'd fix this would be:
requestAnimationFramebehavior. This alone gets the Chromium behavior.scrollendevent like "if there's a pending scroll event for this node, return", to get the Safari behavior.I don't feel too strongly about the Safari vs. Chromium behavior, tho. We can choose which one we want by adding or removing (2).
flackr commentedon Apr 12, 2026
The way this works in chrome is all of the events (i.e. scroll and scrollend) are already enqueued before we start dispatching. This is why even if you start scrolling in response it doesn't prevent the scrollend from being dispatched - since it technically ended before you received the scroll event. IMO this is expected since per spec scroll events are added to a queue and dispatched later.
hiikezoe commentedon Apr 12, 2026
FWIW, if the initial
scrollTopoperation in the test case is done in a requestAnimationFrame callback, such asrequestAnimationFrame(() => { document.scrollingElement.scrollTop = 100; });Safari prints
scroll,scroll,scrollend,scroll,scrollendSo it looks to me that Safari kinda agrees on the Chrome's behavior.
Note that I tested it on iOS 26.4.1.
(EDITED: I originally mis-typed
scrollTop = 200, I am sorry for the confusion)css-meeting-bot commentedon May 6, 2026
The CSS Working Group just discussed
[cssom-view] Clarify number and order of `scroll` and `scrollend` events when a `scroll` event listener performs another scroll, and agreed to the following:RESOLVED: snapshot the event queue and empty it before dispatching all the eventsThe full IRC log of that discussion
<fantasai> scribe+<emilio> flackr: this is about what events you should expect to see when you perform a scroll from a scroll event listener
<emilio> ... I think the expectation is that because scroll events are dispatched after the scroll finished you'll see the queued scrollend from the previous scroll
<emilio> q+
<astearns> ack emilio
<fantasai> emilio: I think that falls out of the current spec, but bit that is not specced is that you copy the queued list of scroll events before dispatching all of them
<fantasai> emilio: We have this scroll event queue
<fantasai> emilio: I think that's the behavior you want?
<fantasai> flackr: Yes, I think so
<fantasai> emilio: So that needs clarification
<fantasai> emilio: Otherwise conceptually you could end up with an almost synchronous scroll event
<fantasai> flackr: completely agree. Copy beforehand.
<fantasai> emilio: That matches rAF etc.
<fantasai> astearns: Does that fit the behavior you thought was correct smfr?
<fantasai> smfr: I think it does result in scroll/scrollEnd/scroll/scrollEnd pattern
<fantasai> smfr: Idk whether Safari behavior is better.
<fantasai> smfr: Would like to hear from authors whether having a scrollEnd in the middle is problematic
<emilio> flackr: keep in mind that new scroll would end up in the next update the rendering
<fantasai> smfr: So split into different frames... maybe that's ok
<ydaniv> I think it's ok
<fantasai> emilio: Proposal is to snapshot the event queue and empty it before dispatching all the events
<flackr> +1
<emilio> PROPOSED: snapshot the event queue and empty it before dispatching all the events
<emilio> RESOLVED: snapshot the event queue and empty it before dispatching all the events
<emilio> astearns: does this cover your second point of checking whether there's a pending scroll event?
<emilio> flackr: I think this covers it
ExtAnimal commentedon Sep 8, 2026
Are any of the browser vendors making any progress on this?
Because I suspect that universally, applications which need to clear a state or take some other action when a scroll is finished use a timer which kicks off on every
scrollevent, cancelling the last timer from the lastscrollevent if any.This is incredibly wasteful.
But the
scrollendevent MUST actually work, and fire when a scroll has completely finished for this pattern to stop being used. Otherwise some states will be locked in place.Even when they all pull their fingers out (And Safari probably will not for another few years), after all browsers have fixed this, event even then we here will not be able to roll out a better, more efficient way of responding to scrollend for another two years after that (Our legacy browser support policy allows use of browsers up to 2 years old)
I might be retired before that happens. I will email my old colleagues to remind them about this issue!
bramus commentedon Sep 8, 2026
@ExtAnimal From Chromium’s side there is https://issues.chromium.org/issues/510319892 that you can track. Hit the +1 in the top right to signal that you are impacted by this, or hit the star (next to the title of the bug) to subscribe to updates. The bug is currently assigned, which is already quite a good thing :)