In 2026, why does a modal's gray overlay cover only the card while the surrounding page is still clickable—is the overlay just not filling the screen?
When a modal's gray overlay covers only the card and the surrounding page is still clickable, it is usually not because the overlay's width or height was set too small. Instead, an ancestor layer carries transform, filter, will-change, or contain, which redefines the containing block for the fixed element, so the overlay changes from 'relative to the viewport' to 'relative to that layer.' In 2026, under common delivery practice, first follow the ancestor chain to confirm what it actually scrolls with, then change the styles; this saves time compared with repeatedly adjusting height and z-index.
Why does the overlay cover only one area as soon as an animation is added?
position: fixed is positioned relative to the viewport by default. But if even one layer in the ancestor chain generates a containing block, the fixed element is positioned relative to that layer instead and moves with the ancestor when the page scrolls. The symptoms are: the overlay covers only the card, the drawer covers only the container, or it disappears when you scroll down. This is behavior within the specification, not a browser bug.
Entrance animations are the most common trigger. translateY for slide-in and scale for pop-in are both implemented through transform, and many animation libraries attach transform directly to the whole container. What is more troublesome is that after the animation ends, the property value is often not none but a terminal state such as translateY(0) or scale(1); the containing block still applies, which leads to 'the animation was removed, but the overlay is still wrong.'
- Only transform: none counts as cleared; translateY(0) does not.
- animation-fill-mode: forwards or inline style can lock in the terminal state.
- will-change: transform can trigger it even if nothing is currently animating.
- filter: blur(0) and backdrop-filter work the same way; as long as the value is not none, it counts.
- contain: paint / layout / strict also generates a containing block; this one is the easiest to overlook.
First confirm what it scrolls with, then change the code
Experience shows that more than half the time is wasted deleting code before confirming the reference point, only to find that another layer is responsible. You can follow four steps; the order is not recommended to be shuffled.
- Set a baseline: add an outline around the overlay, scroll the page, and see which element it moves with; confirm the reference point first.
- Check properties: from the parent upward, inspect transform, filter, perspective, will-change, contain, and backdrop-filter in computed styles layer by layer; any value other than none is a suspect layer.
- Isolate and verify: temporarily set transform: none only on the suspect layer and see whether the overlay returns to full screen; change only one layer at a time, not several places at once.
- Fix at the specific point: choose the smaller change for the business context, and after the change verify once each on iOS Safari, mainstream Android devices, and the WeChat in-app browser.
Step 2 should look at computed styles, not the declarations written in the styles panel, because many transform values are attached at runtime by animation libraries; step 3 only verifies the hypothesis and is not the final solution.
Four fixes: cost ranges and their respective boundaries
- Approach A: push the animation down to an inner content block. Keep the outer container clean while preserving the motion effect. The change surface is small, with a typical range of 0.5–1 person-day; suitable when the animation applies only to a first-screen content block or form card, and not suitable when the design itself requires the whole block to move.
- Approach B: mount both the overlay and the modal to body. Use the framework's portal (React createPortal, Vue Teleport) to render under body; typical range 1–3 person-days, including checks for style scoping and CSS variables. Suitable for admin/back-office projects where the modal is reused in multiple places, and not suitable for legacy code that strongly depends on parent-level variables and scoped styles.
- Approach C: switch to the native dialog element. showModal enters the browser top layer and is not affected by ancestor transform. It is easier for new projects, with a typical range of 1–2 person-days; for existing projects, first check support on target devices and the cost of style resets.
- Approach D: remove the class name when the animation ends. The cost is lower, typically under half a day; however, with fast open/close or repeated clicks, state can easily become out of sync. It is suitable only as a temporary fallback and is not recommended as the only solution.
Comparing them side by side: A has a small change surface and relatively controllable risk; B has more durable benefits, at the cost of one round of style-scope checks; C has lower long-term maintenance cost but requires handling compatibility up front. The judgment can be reduced to one sentence: as long as the animation still sits on the outer container, the other approaches are only workarounds; if you can push it down, push it down; if you cannot, send the overlay and modal through a portal together.
On delivery: the extra half day after preserving the motion effect
In one admin/back-office project, the client required the first-screen card to keep its 'slide in from the top' effect, and the budget allowed only one round of revision; the card also embedded a third-party carousel component. The approach was to push the animation from the whole container down to the card's content layer, and at the same time change the detail drawer to use Teleport mounted to body. The motion effect was preserved, and the overlay returned to full screen; however, several CSS variables inside the drawer were written in the parent scope and could not be read after mounting to body, so colors and border radii all changed. About another half day was spent handling variable scope, and the overall work finished nearly a day later than estimated. The cost of this kind of rework is usually not in changing the code itself, but in not checking in advance which variables and scoped styles the overlay and modal depend on—by the experience range, the extra time roughly falls between half a day and one day, proportional to the degree of style coupling inside the modal.
When to check, and when not to bother
There are roughly three situations that need this troubleshooting: entrance animations are tied to the outer container; the modal or drawer needs to cover the entire viewport; or an H5 page is embedded in a WebView or a mini-program web-view but the overlay behaves inconsistently. Conversely, there are scenarios that can be skipped directly.
- No need: it is only an occasional lightweight toast, and there is no containing-block-generating property anywhere in the ancestor chain; adjusting the structure directly is faster than troubleshooting.
- No need: the overlay is rendered by the App's native layer, so the problem is not in CSS and walking the ancestor chain is meaningless.
- No need: the animation is implemented by Canvas or natively and does not go through CSS containing blocks at all.
- Distinguish first: if the overlay's position and size are correct but it is covered by something else or cannot be clicked, it is most likely a stacking context or event propagation issue, which is not the same as the topic of this article.
Common questions
I changed transform to none, so why is the overlay still not full-screen?
It is commonly because the terminal state is locked by fill-mode: forwards or inline styles, or because will-change or filter is taking effect. First look at the actual computed values, then rule them out layer by layer.
The overlay's position and size are correct, but the button below cannot be clicked—is that also this cause?
Not necessarily. First confirm whether the overlay really covers the full viewport, then check stacking order and pointer-events / event propagation issues; that is a different line of investigation.
After mounting to body with Teleport, the modal styles are all broken—what should I do?
This is mostly caused by style scoping or CSS variables depending on the parent. Move the variables up to :root, or give the modal its own root node layer; this is usually less trouble than changing back to rendering inside the component.
In a mini-program, the same overlay is covered by a native component—should I troubleshoot it the same way?
The direction is different. Native components in mini-programs have a higher layer than ordinary views, so you need to use native overlay capabilities such as cover-view; ancestor transform is not the main issue.
If you are facing exactly this symptom: first use an outline to confirm what the overlay scrolls with, then follow the ancestor chain and check transform, filter, will-change, and contain; after confirming the root cause, choose the smaller-change solution, and do not repeatedly try height and z-index. If the overlay is rendered by a native layer, or the page does not involve a full-screen overlay at all, this troubleshooting can be skipped directly.
-
In 2026, with keep-alive preserving filters and scroll position on a list page, should you refetch every time you return from detail when the data is still stale?
Date: Oct 22, 2026 Read: 6
-
When a React List Page Scrolls, API Calls Fire in Bursts—Is It Because the Object in the useEffect Dependency Array Is Recreated Every Time?
Date: Oct 21, 2026 Read: 10
-
In 2026, the API returns 0 for inventory but the page shows “--”: is the frontend treating 0 as no value?
Date: Oct 20, 2026 Read: 13
-
Is it still worth fixing keyboard focus in AI-generated popups and forms before launch in 2026?
Date: Oct 18, 2026 Read: 18
-
In 2026, I'm the only front-end dev on a small team and want to move toward an experience architect role—should I shore up design aesthetics first or performance and SEO?
Date: Oct 17, 2026 Read: 22




