Problem description
Locally, pnpm dev renders pages as a seamless whole, e.g. http://localhost:4322/tutorial/math-map. But after deploying to production, two problems appeared:
- Whitespace on both sides of the page
- The scrollbar isn’t hugging the rightmost edge of the browser window
Live URL: http://115.191.0.179/tutorial/math-map/
Layout structure review
The page uses the AppLayout.astro layout, structured like this:
body (overflow-hidden)
└── div.flex.h-screen.w-full
├── aside (sidebar, 60px)
└── main.flex-1
├── header (top navigation)
└── div.overflow-y-auto ← scrolling happens here
└── page content
Root cause analysis
Root cause 1: body { overflow: hidden }
Line 60 of AppLayout.astro sets overflow-hidden on the body, delegating scrolling to the inner <div class="overflow-y-auto">.
Consequences:
- The scrollbar appears inside the main content area, not at the rightmost edge of the viewport
- Hard to notice under macOS overlay scrollbars, but very obvious on systems that always show scrollbars
Root cause 2: the flex container’s h-screen constraint
The outer flex container uses h-screen (100vh), locking the entire layout to the viewport height. Combined with the body’s overflow-hidden, this forms an “app-shell” style layout: the body never scrolls; scrolling is entirely handled by the inner div.
Root cause 3: the global button, a { overflow: hidden } style
globals.css contains a global CSS rule intended for button ripple effects:
button, a {
position: relative;
overflow: hidden;
}
This selector is too broad, applying to all <a> and <button> elements. Although its direct impact on sidebar nav links is small, in production builds the merged CSS cascade order can cause unexpected layout interference.
The fix
Change 1: remove overflow-hidden from body
- class="... overflow-hidden selection:bg-blue-500/30"
+ class="... selection:bg-blue-500/30"
Let the body resume normal scrolling; the scrollbar accordingly returns to the rightmost edge of the viewport.
Change 2: use position: fixed for the sidebar
Originally the sidebar was a flex child; combined with sticky positioning it behaved unreliably inside the flex container (inconsistent across browsers). Switch to fixed:
- class="hidden md:flex w-[60px] flex-col ... sticky top-0 h-screen z-20"
+ class="hidden md:flex w-[60px] flex-col fixed left-0 top-0 h-screen ... z-20"
Change 3: flex container becomes min-h-screen, main gets a sidebar offset
- <div class="flex h-screen w-full">
+ <!-- remove the outer flex container; sidebar and main become direct body children -->
- <main class="flex-1 flex flex-col h-screen min-w-0 ...">
+ <main class="min-h-screen flex flex-col ml-0 md:ml-[60px] ...">
Key changes:
h-screen→min-h-screen: short content still fills the viewport at minimum; long content extends naturally- Removed
flex-1(the sidebar is out of document flow, no longer participating in flex space allocation) - Added
md:ml-[60px]to make room for the fixed sidebar (desktop only)
Change 4: remove overflow-y-auto from the inner div
- class={`flex-1 overflow-x-hidden ${flat ? "overflow-hidden" : "overflow-y-auto scroll-smooth"}`}
+ class={`flex-1 overflow-x-hidden ${flat ? "overflow-hidden" : ""}`}
The body now owns scrolling, so the inner div no longer needs overflow-y-auto.
Change 5: narrow the ripple CSS selector
- button, a {
- position: relative;
- overflow: hidden;
- }
+ .ripple-btn, .ripple-link {
+ position: relative;
+ overflow: hidden;
+ }
From a global selector to specific classes; add .ripple-btn or .ripple-link explicitly when a ripple effect is wanted.
Change 6: defensive styles
Add explicit width constraints in globals.css to prevent accidental inheritance:
html, body {
width: 100%;
max-width: 100%;
}
#main-content-container {
max-width: none;
}
Final structure
body ← normal scrolling, overflow-hidden removed
├── aside.fixed.left-0.h-screen ← fixed on the left, always visible
└── main.min-h-screen.md:ml-[60px]
├── header.sticky.top-0 ← sticks to top when scrolling
└── div.flex-1
└── #main-content-container
└── page content
Takeaways
-
The body’s scrolling should be handled by the body itself. Setting
overflow: hiddenon the body and simulating scrolling with an inner div seems clever, but in practice it breaks easily across browsers and devices. -
position: fixedis more reliable thanposition: stickyinside flex containers.stickybehaves inconsistently on flex children, especially when the parent’s height changes dynamically. -
Be cautious with global CSS selectors. A rule like
button, a { overflow: hidden }has too wide an impact; use specific classes to control it. -
Watch for differences between local dev and production. Vite’s dev server injects CSS differently than the merged order after
astro build, so the same code can behave differently in the two environments.