A CSS filter on an ancestor cancels an in-progress touch scroll

Reduced test case for WebKit bug 322849 (rdar://problem/186671580). Affects iOS Safari. Not reproducible in Chrome on Android, or in any desktop browser including desktop Safari.

The box below scrolls. Its parent — never the scroller itself — has a filter. With the filter present, touch drags inside the box scroll nothing at all about ninety-five per cent of the time; with it gone, every drag scrolls. The page scores itself: drag a few times in each mode and read the verdict.

Drag inside the box a few times.
Filter on the ancestor

Pixels scrolled, this gesture0
Gestures that scrolled0
Gestures that did nothing0
Median pixels per gesture—

Measured on an iPhone 16 Pro, iOS 26.6

Off (control)0 of 50 gestures failed
On :active49 of 50 failed
Changed during the drag47 of 50 failed

Both filter modes fail about as often as each other — 49 and 47 out of 50 is the same result within sampling noise — and the control does not fail at all. The third mode was added on the theory that re-setting the filter throughout the gesture would remove the timing question and make it fail every time. It did not, so that theory is wrong: whatever decides the outcome is not simply how much of the gesture the filter change overlaps.

A suggested workaround, under test

The fourth mode adds contain: contents to the element the filter is applied to — shorthand for layout, paint and style containment. The reasoning, suggested by u/maven_peace on r/webdev: containment establishes the containing block and stacking context up front, so gaining a filter no longer restructures the layers above the scroller, and there is nothing to make the UIScrollView be removed and re-added. That fits the mechanism described in the bug exactly. Whether it holds in practice is what the mode is for; if it does, it is a workaround real pages can use today.

Both modes are kept because that null result is itself worth having. What is solid is the contrast with the control: fifty clean gestures with no filter on the ancestor, and ninety-odd per cent failure with one.

/* the reported case, and the stronger of the two */
.host        { transition: filter 150ms ease-out; }
.host:active { filter: brightness(0.95); }

/* the other shape, which fails at the same rate */
host.style.filter = 'brightness(' + (0.95 + n) + ')';   // on every touchmove

Steps

  1. Open this page on an iPhone, in Safari.
  2. Select Off and drag inside the box five times. Every drag should scroll, and the verdict should read as expected.
  3. Select On :active and drag five times the same way.
  4. Compare the two verdicts, and the median pixels per gesture. The third mode is there if you want it, and behaves like the second.

Five gestures per mode is enough to see it. The numbers above came from fifty each.


Filed as WebKit bug 322849, imported to Radar as rdar://problem/186671580. Written up in Safari dropped my scroll because of a CSS fade on an ancestor, which is where it came from — a reader called ReadKinetic that draws real typeset PDF pages in the browser.