Your extension popup opens to a 10 px sliver, balloons to show a native scrollbar on a single-line interface, or clips a button that was visible in DevTools but invisible in the live popup. These are among the most disorienting bugs in extension development because the popup’s size is controlled entirely by CSS — there are no manifest keys for dimensions — and the browser host reads that CSS before you can inspect it. This guide is part of Popup Interface Design and walks through every size and overflow failure mode with the exact CSS fix.
Unlike a normal web page where the viewport drives the layout, a Manifest V3 popup works in reverse: the browser creates a native window and then sizes it to match the rendered <body> element’s box model. Two consequences follow:
- If
<body> has no declared width, the popup collapses to the intrinsic minimum width of its content — often zero or a few pixels for empty containers. - If any element inside
<body> is wider or taller than 800 × 600 px, the browser adds native OS scrollbars that cannot be suppressed with CSS alone.
The fix is always the same principle: declare explicit bounds on <body> before content loads, never rely on percentage widths or 100vw/100vh, and prevent individual elements from overflowing the container.
toolbar click → renderer created (0×0) → popup.css parsed
→ <body> box computed → window sized to match body
├─ body.width < 280px → sliver
├─ body.height > 600px → vertical OS scrollbar
└─ any child > 800px → horizontal OS scrollbar
→ DOMContentLoaded → storage read → DOM update
└─ reflow changes body size → window jumps
Step 1: Set explicit body dimensions to prevent collapse
The most common cause of a popup rendering too small is a missing width declaration on <body>.
1/* popup.css */
2*, *::before, *::after {
3 box-sizing: border-box; /* critical: prevent padding from adding to width */
4}
5
6body {
7 margin: 0; /* remove browser default 8px margin */
8 min-width: 320px; /* floor — prevents collapse on low-content states */
9 width: 360px; /* target width; drives the native window width */
10 max-height: 560px; /* ceiling — stays inside the 600px hard cap */
11 overflow-x: hidden; /* suppress horizontal scrollbar from sub-pixel rounding */
12 overflow-y: auto; /* allow vertical scroll if content exceeds max-height */
13}
Execution context: Applied in the popup renderer process. The browser measures the <body> bounding box after the CSSOM is built but before JavaScript runs. A min-width guarantees a usable window even when content is absent (e.g., during the async storage load). Without box-sizing: border-box, a width: 360px body with padding: 16px renders as 392 px wide, pushing the popup into the 400 px range and potentially triggering overflow.
Step 2: Handle the 800 × 600 maximum ceiling
The hard ceiling of 800 × 600 px is enforced by the browser host, not by CSS. You cannot override it with max-width, !important, or any JavaScript call. If your rendered content exceeds either dimension, the OS draws native scrollbars inside the popup window — these consume space (typically 15–17 px on Windows, 0 px on macOS with overlay scrollbars) and can cause a cascading layout shift.
The fix is to set max-height on <body> to a value safely below 600 px and contain internal scrolling within the popup rather than letting it overflow to the window.
1body {
2 max-height: 560px; /* 600px cap minus ~40px OS frame safety margin */
3 overflow-y: auto; /* internal scrollbar, not OS window scrollbar */
4}
5
6/* For Firefox — native scrollbars are wide by default */
7body {
8 scrollbar-width: thin;
9 scrollbar-color: #94a3b8 transparent;
10}
Execution context: Rendered in the popup page context. On macOS with “Show scroll bars: When scrolling” the OS scrollbar is overlay-style and consumes no space — but on Windows and Linux, scrollbar-width: thin prevents the internal scrollbar from adding ~15 px to the effective content area width. Safari on macOS follows the same overlay-scrollbar behaviour. Test on Windows to catch this class of bug.
Step 3: Fix content clipping from missing overflow declarations
Content is clipped (rendered but not visible) when a parent container has overflow: hidden — explicitly or via a stacking context — but the parent is smaller than the content. The popup’s max-height combined with a flex column layout is a common trigger.
1/* Avoid this — clips content at max-height without scroll */
2body {
3 max-height: 560px;
4 overflow: hidden; /* ← content below 560px is silently clipped */
5}
6
7/* Fix — allow scrolling inside the popup */
8body {
9 max-height: 560px;
10 overflow-y: auto; /* ← scroll instead of clip */
11 overflow-x: hidden; /* ← still suppress horizontal overflow */
12}
Execution context: Computed in the popup renderer. overflow: hidden is a common copy-paste error from code that intended to suppress scrollbars but instead clips content. If only part of your UI is clipped — for example, a dropdown that renders under the popup’s bottom edge — the issue is a nested overflow: hidden on a parent container rather than <body>. Use DevTools “Computed” panel to find which ancestor triggers the clip.
Step 4: Prevent dynamic content reflow from resizing the window
The most confusing size bug: the popup opens at the correct size, then jumps larger or smaller 50–200 ms later when JavaScript updates the DOM after the storage read completes. This happens because the native window responds to body size changes.
Prevent reflow-driven resizing by locking the body dimensions before any JS runs:
1/* popup.css — loaded before popup.js */
2body {
3 width: 360px; /* fixed — does not change with content */
4 min-height: 400px; /* reserve space so content injection does not shrink body */
5 max-height: 560px;
6 overflow-y: auto;
7}
And in JavaScript, batch DOM updates after the storage read into a single requestAnimationFrame call:
1// popup.ts
2document.addEventListener('DOMContentLoaded', async () => {
3 renderSkeleton(); // paint skeleton before awaiting storage
4
5 const { settings, items } = await chrome.storage.local.get(['settings', 'items']);
6
7 requestAnimationFrame(() => {
8 // Replace skeleton in one frame — prevents incremental reflow
9 const container = document.getElementById('content')!;
10 container.innerHTML = buildItemsHTML(items ?? []);
11 document.getElementById('skeleton')?.remove();
12 });
13});
Execution context: Runs in the popup renderer. requestAnimationFrame defers the DOM swap to the next paint frame after the browser has committed the initial layout, eliminating the visible window-resize jump. chrome.storage.local.get resolves directly in the popup context — no service worker wake-up required.
Step 5: Account for devicePixelRatio on high-DPI displays
On 2× Retina displays a 360 px CSS popup is 720 physical pixels — within the cap. A 480 px popup becomes 960 physical pixels and may be cropped on small screens or repositioned by the OS window manager. If you support variable-width designs, cap logical width at high DPR:
1// popup.ts
2const dpr = window.devicePixelRatio ?? 1;
3const MAX_W = dpr >= 2 ? 360 : 480;
4(document.getElementById('app') as HTMLElement).style.maxWidth = `${MAX_W}px`;
Execution context: Popup renderer after DOMContentLoaded. window.devicePixelRatio is always available in extension pages and is not subject to content-script cross-origin restrictions. Common values: Chrome/macOS Retina = 2, Windows 1440p = 1.5, Firefox follows the OS scale factor (can be 1.25). Inspect DPR by opening the popup’s own DevTools (right-click popup → Inspect) — the main browser DevTools does not show popup console output.
Cross-browser variation
- Chrome / Edge: The 800 × 600 cap is enforced pixel-exactly.
box-sizing: border-box is required globally or the declared width does not match the rendered window size. Chrome 109+ displays a scrollbar reservation gap even on macOS when overflow-y: auto is set and the content height is near max-height; add scrollbar-gutter: stable to reserve the gap consistently. - Firefox: Renders wide native scrollbars on Linux and Windows by default — a 360 px popup with a visible scrollbar shows 343 px of usable content width.
scrollbar-width: thin reduces the scrollbar to ~6 px. Firefox also enforces a minimum popup height of approximately 80 px; content shorter than this is padded by the browser. - Safari: Uses
SFSafariExtensionViewController — the popup is a native WKWebView. The 800 × 600 limit applies, but Safari’s WebKit rendering engine handles sub-pixel dimensions differently: a width: 360.5px computed value may round to 361 px, shifting the window by 1 physical pixel. Use integer pixel values throughout. Safari 16 and earlier do not support scrollbar-gutter. - Arc / Brave: Inherit Chromium’s behaviour. Arc applies a rounded corner clip to the popup window visually, which can hide content within ~8 px of the popup edge — increase body padding to at least 12 px on all sides.
Verification
- Load the unpacked extension in Chrome, open the popup, and right-click → Inspect. In Elements, select
<body> and read the computed width, max-height, and overflow values — they must match your CSS declarations. - In the Console run
document.body.scrollWidth > document.body.offsetWidth — must return false (no horizontal overflow). Run document.body.scrollHeight > document.body.offsetHeight to confirm vertical overflow is contained by the internal scroll, not the OS window. - Set Windows display scale to 125% and reopen the popup. Confirm no content is clipped and no unexpected OS scrollbar appears.
- In Firefox, confirm
scrollbar-width: thin in the computed styles panel to ensure the wide native scrollbar is not consuming layout width. - Record a DevTools Performance trace from toolbar click to
DOMContentLoaded end — target under 80 ms.
FAQ
<body> has no declared width, so the browser renders it at intrinsic minimum — often a few pixels for a bare DOM. Add width: 360px and min-width: 320px to body, and confirm the stylesheet link appears before the <script> tag so dimensions are set before the first paint.
This mismatch occurs when you open the popup HTML directly (chrome-extension://id/popup.html) rather than clicking the toolbar icon. DevTools in that mode does not apply the popup window size constraints. Always test through the toolbar button.
No — the 800 × 600 ceiling is host-enforced and cannot be overridden. Move content that needs more vertical space to an options page (chrome.runtime.openOptionsPage()) — see Options Page Layouts.
Set min-height on <body> to match your skeleton height. When real content replaces the skeleton at the same height the window stays stable. If real content can be taller, use a fixed height and let internal content scroll within that bound.