bvaughn / bvaughn/react-resizable-panels

Panel layout gets permanently corrupted on iPadOS Safari after backgrounding the app (transient resize burst skews percentage, doesn't self-correct)

Open
#731 2 comments 0 reactions 0 assignees View on GitHub
discussion needs information
Dominant language
TypeScript
Stars
5.4k
Forks
228
Avg merge
2d 10h
Merged PRs (30d)
3

Description

**Version:** react-resizable-panels@4.12.2
**Platform:** iPadOS Safari (reproduces both as regular Safari UA and as the "Macintosh" desktop-site UA iPadOS sometimes reports)

## Summary

After a user manually resizes a `Panel` inside a `Group` (drag), backgrounding the Safari tab (e.g. switching to another app via the App Switcher) and then returning to it can leave the panel's live layout visually corrupted — a `Panel` with a pixel `minSize` ends up much larger than the user set, and does not shrink back even though the container has returned to its original width. A full page reload restores the correct layout, which confirms only the *live* layout state is affected, not anything persisted via `useDefaultLayout`/`localStorage`.

## What we found

On-device console logging (attaching listeners for `resize`, `orientationchange`, `visibilitychange`, and a `ResizeObserver` on the `Group` element) showed:

1. Right as the app starts moving to the background (well before `visibilitychange` fires `"hidden"`), a burst of `resize`/`orientationchange` events fires with transiently wrong container widths — in our case the sequence was roughly `1180px → 820px → 585px → 1180px` within ~700ms (this looks like it's coming from the OS-level app-switch transition animation, not a real resize).
2. During the transient narrow reading (e.g. 540px), a `Panel` with `minSize={380}` no longer fits at its current percentage, so the library recomputes a much larger percentage to satisfy the pixel `minSize` (e.g. jumping from ~49% to ~72%).
3. Once the container returns to its real width (1180px again), that skewed percentage is **not** re-derived — because the default `groupResizeBehavior` is `"preserve-relative-size"`, the *percentage* (not the pixel size) is what's preserved across the resize, so the panel stays pinned at the inflated percentage even though nothing about the current width requires it anymore.
4. In our case this all happens **before** `visibilitychange` turns `"hidden"` — in one capture, the last `resize` event and `visibilitychange:"hidden"` were only ~15ms apart. We were unable to observe `onLayoutChanged` firing at all for this transient burst in that scenario (it's possible the notification is scheduled via `requestAnimationFrame` internally and never gets a chance to run before rAF is suspended for the hidden document) — so consumers can't reliably detect the corruption via the public callback API either.
5. Once the tab becomes `"visible"` again, no further resize/layout event fires at all — the container's width is already correct at that point but the skewed percentage from step 3 is simply left as-is, with nothing to trigger a recompute.

We were able to reproduce this consistently by: dragging a `Separator` to resize a `Panel` with a pixel `minSize`, then switching to another app via iPadOS's App Switcher and back to Safari.

## Minimal repro

```tsx
import { Group, Panel, Separator, useDefaultLayout } from "react-resizable-panels";

function App() {
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
id: "demo",
panelIds: ["sidebar", "main"],
storage: localStorage,
onlySaveAfterUserInteractions: true,
});

return (




sidebar



main



);
}
```

Steps on an iPad (Safari):
1. Open the page above.
2. Drag the separator to make the sidebar noticeably narrower or wider than its default 380px.
3. Swipe up to the App Switcher, open a different app, then switch back to Safari.
4. The sidebar's width has changed significantly from what was set in step 2 (usually much wider); a full page reload restores the correct width.

## Workaround (application level)

We worked around this by listening for `document.visibilitychange` turning `"visible"` and imperatively calling the `Group`'s `setLayout()` back to the last user-set layout (tracked via `onLayoutChanged`'s `isUserInteraction` flag) if it differs from the panel's current `getLayout()`. This isn't something library consumers should have to do, though — a library-level fix (e.g. ignoring/debouncing resize readings that are followed almost immediately by a very different one, or treating a `minSize`-driven percentage bump as provisional until the resize settles) would remove the need for it.

Happy to share the full console log timeline if useful.

Contributor guide

Open the contributing guide

Research direction

Start with the Group resize handling and the Group/Panel layout APIs used in the minimal React reproduction, especially setLayout, onLayoutChanged, and pixel minSize behavior. Reproduce the sequence on iPadOS Safari while observing resize and visibilitychange events, then trace how transient widths alter the preserved percentage. Done means returning from the App Switcher without permanently changing the user's layout, with coverage for the reported repro.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.