What package is this bug report for?
@headlessui/vue
What version are you using?
Latest main (checked packages/@headlessui-vue/src/hooks/document-overflow/overflow-store.ts and handle-ios-locking.ts directly)
Reproduction URL
N/A — source-level finding, see mechanism below. I can put together a repro if useful.
Describe your issue
@headlessui-react fixed this exact bug in #3796 (merged 2025-09-17, "Ensure sibling Dialog components are scrollable on mobile"), but the Vue package never received the port.
Note: this is a different, more specific bug than the one tracked in #3584. #3584 is about the top-layer/stack architecture from #3242 (which sibling Dialog is "active" for Escape/outside-click) — that's a separate mechanism in a separate file. This issue is only about the iOS scroll-lock meta staleness in overflow-store.ts/handle-ios-locking.ts, fixed by #3796, unrelated to #3242's stack.
The bug
overflow-store.ts's SCROLL_PREVENT handler computes the allowed-containers meta once, at the moment the document transitions from unlocked to locked:
SCROLL_PREVENT({ doc, d, meta }: DocEntry) {
let ctx = {
doc,
d,
meta: buildMeta(meta),
}
...
}
SCROLL_PREVENT only fires when willChange is true (the lock count goes from 0 to something >0). If a second Dialog/Popover/etc. opens while the document is already locked, only PUSH runs (updates entry.meta), SCROLL_PREVENT does not run again — so handle-ios-locking.ts's inAllowedContainer() closure keeps referencing the stale meta captured from only the first overlay's containers:
// handle-ios-locking.ts, current Vue source
function inAllowedContainer(el: HTMLElement) {
return meta.containers
.flatMap((resolve) => resolve())
.some((container) => container.contains(el))
}
Concretely: open a Dialog, then open a second Dialog on top of it (both stay mounted — e.g. a confirm dialog over a form dialog). On iOS Safari, touches inside the second dialog aren't recognized as "inside an allowed container" (since its containers were never added to the frozen meta), so they get touchAction: none/preventDefault'd, making the second dialog unscrollable.
The fix that already landed for React (#3796)
Makes meta a lazily-evaluated function instead of a frozen snapshot, recomputed on every PUSH/POP:
// overflow-store.ts
interface DocEntry {
...
computedMeta: Record<string, any>
}
PUSH(doc, meta) {
...
entry.computedMeta = buildMeta(entry.meta)
...
},
POP(doc, meta) {
...
entry.computedMeta = buildMeta(entry.meta)
...
},
SCROLL_PREVENT(entry: DocEntry) {
let ctx = {
doc: entry.doc,
d: entry.d,
meta() {
return entry.computedMeta
},
}
...
}
// handle-ios-locking.ts
function inAllowedContainer(el: Element) {
for (let resolve of meta().containers) {
for (let element of resolve()) {
if (element.contains(el)) return true
}
}
return false
}
I confirmed the Vue package's overflow-store.ts/handle-ios-locking.ts still have the pre-#3796 shape (meta: buildMeta(meta) computed once in SCROLL_PREVENT, meta used as a plain object rather than a function in inAllowedContainer) — the Vue changelog has no entry corresponding to #3796.
Expected behavior
Same fix as #3796, ported to @headlessui-vue.
What package is this bug report for?
@headlessui/vue
What version are you using?
Latest
main(checkedpackages/@headlessui-vue/src/hooks/document-overflow/overflow-store.tsandhandle-ios-locking.tsdirectly)Reproduction URL
N/A — source-level finding, see mechanism below. I can put together a repro if useful.
Describe your issue
@headlessui-reactfixed this exact bug in #3796 (merged 2025-09-17, "Ensure siblingDialogcomponents are scrollable on mobile"), but the Vue package never received the port.Note: this is a different, more specific bug than the one tracked in #3584. #3584 is about the top-layer/stack architecture from #3242 (which sibling
Dialogis "active" for Escape/outside-click) — that's a separate mechanism in a separate file. This issue is only about the iOS scroll-lockmetastaleness inoverflow-store.ts/handle-ios-locking.ts, fixed by #3796, unrelated to #3242's stack.The bug
overflow-store.ts'sSCROLL_PREVENThandler computes the allowed-containersmetaonce, at the moment the document transitions from unlocked to locked:SCROLL_PREVENTonly fires whenwillChangeis true (the lock count goes from 0 to something >0). If a secondDialog/Popover/etc. opens while the document is already locked, onlyPUSHruns (updatesentry.meta),SCROLL_PREVENTdoes not run again — sohandle-ios-locking.ts'sinAllowedContainer()closure keeps referencing the stalemetacaptured from only the first overlay's containers:Concretely: open a
Dialog, then open a secondDialogon top of it (both stay mounted — e.g. a confirm dialog over a form dialog). On iOS Safari, touches inside the second dialog aren't recognized as "inside an allowed container" (since its containers were never added to the frozenmeta), so they gettouchAction: none/preventDefault'd, making the second dialog unscrollable.The fix that already landed for React (#3796)
Makes
metaa lazily-evaluated function instead of a frozen snapshot, recomputed on everyPUSH/POP:I confirmed the Vue package's
overflow-store.ts/handle-ios-locking.tsstill have the pre-#3796 shape (meta: buildMeta(meta)computed once inSCROLL_PREVENT,metaused as a plain object rather than a function ininAllowedContainer) — the Vue changelog has no entry corresponding to #3796.Expected behavior
Same fix as #3796, ported to
@headlessui-vue.