fix(VOverlay): keep overlay attached with iOS keyboard open - #22923
Merged
J-Sek merged 2 commits intoJun 13, 2026
Conversation
With the software keyboard open, getElementBox()'s documentElement branch adds visualViewport.offsetTop to the overflow-clamp viewport, while the activator box stays in the layout frame from getBoundingClientRect. The two frames diverge by offsetTop and the overlay freezes off its activator on scroll. The correct clamp origin is the visual-viewport top in the layout frame: offsetTop on Blink, but 0 on iOS WebKit, where the layout viewport itself slides under the keyboard. Gate the origin on IS_WEBKIT so WebKit uses 0 and Blink keeps offsetTop. scale > 1 still returns 0. fixes vuetifyjs#22376
J-Sek
force-pushed
the
fix/22376-ios-keyboard-overlay-drift
branch
from
June 13, 2026 10:50
8125f6c to
a8616c6
Compare
J-Sek
approved these changes
Jun 13, 2026
J-Sek
pushed a commit
that referenced
this pull request
Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
With the iOS/iPadOS software keyboard open, VMenu, VAutocomplete, VCombobox and VSelect dropdowns
drift off their activator as you scroll: they freeze, lag, or appear to scroll opposite the page,
and the keyboard can end up covering the options. Reproduces on v4.1.1.
The cause is a mixed coordinate basis in
connectedLocationStrategy. With the keyboard up,getElementBox(document.documentElement)addsvisualViewport.offsetTopto the overflow-clampviewport, while the activatortargetBoxcomes straight fromgetBoundingClientRectand staysin the layout frame. The two boxes diverge by exactly
offsetTop, the clamp top lands below theactivator, and the overflow-shift logic pins the content there while the activator scrolls away.
The clamp origin should match the frame
targetBoxlives in. On iOS WebKit the layout viewportitself slides under the keyboard, so that origin is
0, notoffsetTop. The fix gates theexisting ternary in
getElementBox()'sdocumentElementbranch on the engine:An unconditional
x: 0, y: 0would regress Android/Blink: the layout viewport does not movethere, so
offsetTopis the correct origin (what #21462 needed). With the gate, the Blink branchevaluates exactly as today; only iOS WebKit changes. Numeric traces below are from an iPad Air
13" M4 simulator (iPadOS 26.5); the fix was additionally verified on a physical iPad Air 11-inch
(M4), where the overlay stays anchored with the keyboard open.
fixes #22376
On-device evidence
iPad Air 13" M4 simulator, iPadOS 26.5, real WebKit, software keyboard up,
scale === 1,visualViewport.offsetTop = 403(plus the behavioral pass on the physical iPad Air 11" M4 above).Internal
updateLocation()values and painted positions, traced before and after the fix:offsetTop)0viewport.topstickToTargetedge-clippingA separate A/B run isolated three clamp-origin policies on Case A (VMenu + text field), keyboard
up, after a single small scroll with the button still visible (
scrollY ~ 762,offsetTop ~ 403):offsetTop(current master)fixed)The third row matters: a faithful Floating UI carve-out behaves identically to plain zeroing,
because Vuetify's connected overlay is
position: absolute(the trace's.v-overlay__contentreports
absolute), so thefixed-strategy branch never fires.I have only measured iOS — there is no separate Android trace here. There is also no automated
test: jsdom stubs
visualViewportas an emptyEventTargetand headless browsers don't drive thekeyboard, which is why recent VOverlay positioning fixes merged without tests.
Why the ternary has to stay (commit history)
The
scale > 1 ? 0 : offsetLeft/offsetTopternary stacks three fixes; replacing it with aconstant would unwind the first:
6523eee4dfix(VMenu): position relative to visualViewport when not zoomed32242a353feat(locationStrategies): support CSS zoom3085a93e4fix(VOverlay): keep content attached to the activator on page with zoom#21462 was an Android Chrome keyboard bug. Its scroll-chaining symptom was cured by the clamp
height (visible-region height feeding the dropdown max-height), which this change keeps; correct
placement on Android still needs origin and size in one consistent layout frame, i.e. origin
offsetTop. An unconditional0would introduce a new Android positioning error in the samekeyboard-open scenario, so the branch is gated rather than removed.
Floating UI precedent
CSS.supports('-webkit-backdrop-filter', 'none')is Floating UI'sisWebKit(), shipped today byRadix, Element Plus and other
@floating-ui/domconsumers. It catches iOS Chrome and iOS Firefox(also WebKit, same keyboard behaviour), returns
falseon Blink (Chromium shippedbackdrop-filterunprefixed in 76 and never aliased the-webkit-form), and is a capabilityprobe in the same
CSS.supportsguard shape as thesupportsSelectorconst insidematchesSelector()inhelpers.ts.The offset policy here is Vuetify's own frame-symmetry argument —
getOverflow(content, viewport)is only meaningful if both operands share a frame, and
targetBox/contentBoxare rawgetBoundingClientRect(layout frame) — but it agrees with Floating UI:getViewportRect(el, strategy)adds
visualViewport.offset{Left,Top}only when!isWebKit() || strategy === 'fixed', so forVuetify's
position: absoluteconnected overlay its rule also zeroes on WebKit.Alternatives considered & residual risks
position: fixed; top: 0probe to infer the engine: atransform,filter,containorwill-changeon an ancestor makes the probe resolve against that ancestor (the containing-blocktrap — Floating UI ships a
getContainingBlock()traversal because of it). TheCSS.supportsprobe reads an engine constant and touches no layout.
x: 0, y: 0: the platform swap covered in the description and commit historyabove.
scale, or UA sniffing: the real axis is the painted frame (engine), and UAstrings miss iOS Chrome/Firefox.
IS_WEBKIT, but the change is observable only whenscale === 1andoffsetTop/offsetLeftis non-zero — a panned visual viewport, dominantly the soft keyboard.interactive-widget=resizes-visualby default) may itself want origin0; thegate deliberately preserves today's Blink behaviour pending a real-device measurement, so any
such drift is pre-existing and unchanged by this PR.
IS_WEBKITwould misfire if a future Blink aliased the-webkit-form; it does not today (checked against
css_properties.json5, MDN BCD and caniuse).offsetTop = 0), pinch-zoom (scale > 1alreadyreturned
0) and CSS body zoom ([Bug Report][3.7.4] Incorrect Tooltip Positioning When Body's Zoom is Set to 0.7 #20719 / [Bug Report][3.10.10] Vuetify is miscalculating v-overlay__content position if body element have zoom property. #22326 ridecurrentCSSZoomandwidth/height).Only the x/y origin of
getElementBox(documentElement)changes;width/heightand the%size resolver are untouched.
Repro notes: this does not show in
play.vuetifyjs.com(its iframe masks thevisualViewportoffset) — use a real device or the iOS Simulator with the software keyboard (Cmd+Shift+K), and
tap the field to raise the keyboard.
Markup: