Skip to main content
Zhimalab
中文

Fixing Page Whitespace on Both Sides and Scrollbar Offset Issues

2026-07-12 · 14 min

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:

  1. Whitespace on both sides of the page
  2. 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-screenmin-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

  1. The body’s scrolling should be handled by the body itself. Setting overflow: hidden on the body and simulating scrolling with an inner div seems clever, but in practice it breaks easily across browsers and devices.

  2. position: fixed is more reliable than position: sticky inside flex containers. sticky behaves inconsistently on flex children, especially when the parent’s height changes dynamically.

  3. Be cautious with global CSS selectors. A rule like button, a { overflow: hidden } has too wide an impact; use specific classes to control it.

  4. 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.