A Leftover sticky top:0 Makes a fixed Bar Fill the Screen
This article may contain affiliate links. Its content is not affected by advertising.
In short
Switch from position: sticky to position: fixed without clearing top: 0, and it coexists with bottom: 0 to stretch the element to full viewport height — an accidental full-screen overlay.
Conclusion
Leave the top: 0 you put in for position: sticky in place while a media query switches to position: fixed, and it coexists with bottom: 0 to stretch the element from top to bottom. We hit this on a component whose tab bar was stuck to the top on desktop and became a fixed bottom bar on mobile. The mobile block added bottom: 0 but the base top: 0 survives unless a media query overrides it. The result was a tab bar meant to sit at the bottom spanning the entire screen and swallowing every other interaction. The fix is one line — top: auto in the mobile block — but the CSS is valid from the start, so nothing short of looking at it in a browser reveals it.
Symptom
On desktop the component shows a tab bar at the top of the screen, kept in place while scrolling with position: sticky. On mobile (640px and below) it becomes a fixed bottom navigation, with position: fixed and bottom: 0 set inside a media query.
What was reported in production on 2026-06-15 was the mobile bottom bar stretching across the whole screen, covering everything else and making the page unusable. There were no changes to the DOM structure or to the JavaScript; CSS was the only suspect.
Cause
The base rule (desktop) before the fix:
/* apps/web/app/app/editor/[classId]/_components/ClassEditorShell.module.css (pre-fix, lines 13-25) */
.tabBar {
display: flex;
gap: 0.25rem;
padding: 0.25rem;
margin-bottom: 1rem;
background: #f7f8fa;
border-radius: 0.6rem;
/* Desktop: keep the tabs pinned to the top while scrolling */
position: sticky;
top: 0;
z-index: 30;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
And the mobile media query, before the fix:
/* apps/web/app/app/editor/[classId]/_components/ClassEditorShell.module.css (pre-fix, lines 56-68) */
@media (max-width: 640px) {
.tabBar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
margin-bottom: 0;
border-radius: 0;
border-top: 1px solid #e5e7eb;
background: #fff;
padding: 0.25rem 0.25rem calc(0.25rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
}
In the CSS cascade, any property the media query does not override keeps its value from the base rule. The mobile block overrides position, left, right and bottom, but never touches top. So the base top: 0 remains in effect on mobile and .tabBar renders with position: fixed; top: 0; bottom: 0;.
A position: fixed element with both top and bottom set and no explicit height stretches to fill the gap between them. The tab bar became a box spanning from the top of the viewport (top: 0) to the bottom (bottom: 0). The tabs themselves were laid out at the top by flex, so it was not obvious visually, but the element carried a click area covering the entire screen and absorbed every tap and click meant for other UI.
The fix
Add one line, top: auto, to .tabBar in the mobile block.
/* apps/web/app/app/editor/[classId]/_components/ClassEditorShell.module.css (post-fix, lines 56-72) */
@media (max-width: 640px) {
.tabBar {
position: fixed;
/* Always clear the base sticky top:0. With fixed, top:0 and bottom:0 together stretch
* the height to the full viewport, so the footer covers the screen and blocks every
* interaction (production incident 2026-06-15). top:auto keeps it bottom-fixed only. */
top: auto;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
margin-bottom: 0;
border-radius: 0;
border-top: 1px solid #e5e7eb;
background: #fff;
padding: 0.25rem 0.25rem calc(0.25rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
}
With top: auto the browser treats top as unconstrained. Only bottom: 0 remains as a constraint, so the height returns to the content — the tabs — and the element sits against the bottom edge as intended. The base sticky top: 0 is still needed for desktop, so the base rule is untouched and the mobile block cancels it explicitly.
The change is CSS only; no schema, type or logic changes.
Preventing a repeat
This reached production because position: fixed; top: 0; bottom: 0; is not wrong as CSS. Type checking and the build do not inspect declaration values, and no warning is produced when top and bottom are both in effect. The browser draws exactly the stretched box it was asked for, so only looking at the rendered result finds it.
When writing a media query that switches position, check whether any offset property the new position depends on — top, right, bottom, left — is still set from the base rule. The fix leaves top: auto with a comment, so the next person does not have to work out again why that one line is there.
Frequently asked questions
Q1Why did adding bottom:0 cover the whole screen?
A fixed element with both top and bottom set stretches to fill the gap. The base rule for .tabBar had position: sticky; top: 0 for desktop, and the mobile media query added position: fixed and bottom: 0 without resetting top. The inherited top: 0 made the box span the full viewport.
Q2Why didn't the build or tests catch it?
The combination is valid CSS and is not something type checking or the build inspects. The browser honours top: 0 and bottom: 0 exactly as written, so nothing short of looking at the rendered result on a device or in a browser would reveal the stretch.
Q3What did the fix change?
One line: top: auto on .tabBar inside the mobile media query. With position: fixed kept and only bottom: 0 in effect, the browser is free to size the element by its content, so the height returns to the bar itself.
Environment verified
- Next.js ^16.0.0 / CSS Modules
- Occurred in production 2026-06-15, fixed the same day
What this article is based on
- CSS file lines 13-25commit 3b4ea03
- CSS file lines 56-68commit 3b4ea03
- CSS file lines 56-72commit 98d3816
Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.