Adaptive UI for Chrome Extensions: Building Responsive Popups, Sidepanels and Content Scripts (2026)
Chrome extension UIs exist in a uniquely constrained world. Your popup might be crammed into a 300px-wide panel one moment and stretched across a floating sidepanel the next. Content script overlays get injected into websites with wildly different layouts, font scales, and color schemes. Users run Chrome on 4K monitors, 1080p laptops, high-DPI displays, and everything in between.
Yet most extension tutorials still treat UI as an afterthought — a fixed-size popup that looks fine on the developer’s screen and breaks everywhere else.
This guide covers how to build genuinely adaptive extension UIs using modern CSS techniques, including container queries, logical properties, and prefers-color-scheme — so your extension looks polished no matter where it runs.
Why Adaptive UI Matters for Extensions
Before diving into code, it is worth understanding the unique constraints that make extension UI different from regular web development.
Popups have hard width limits. Chrome enforces a maximum popup width of 800px and height of 600px. But most users never resize their popup — it defaults to whatever width you define in CSS. Too narrow and your UI feels cramped. Too wide and it looks awkward next to the browser toolbar.
Sidepanels are a different surface. The Chrome Side Panel API (introduced in Chrome 114) gives your extension a persistent panel docked to the side of the browser. Users can resize this panel horizontally, so your layout needs to adapt gracefully between a narrow ~250px state and a wider ~500px state.
Content scripts share the page. Overlays and injected UI live inside a host page you don’t control. The host page might have font-size: 18px set globally, or a z-index war, or a dark background that makes your light-themed widget unreadable.
Display density varies. A user on a Retina/HiDPI display at 2x scaling effectively has half the logical pixels. Icons, borders, and images need to look sharp at multiple resolutions.
Adaptive UI means building for all of these conditions from the start, not bolting on fixes after users complain.
CSS Container Queries: The Right Tool for Extension Layouts
Media queries check the viewport width, which is not very useful inside an extension popup (the viewport is the popup). Container queries check the size of a specific element, which maps perfectly to extension UI surfaces.
Setting Up Container Queries in Your Popup
/* popup.css */
/* Define the popup root as a container */
.popup-root {
container-type: inline-size;
container-name: popup;
width: 100%;
min-width: 280px;
max-width: 600px;
}
/* Default: compact single-column layout */
.popup-grid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
padding: 12px;
}
/* Wider popup: two-column layout */
@container popup (min-width: 400px) {
.popup-grid {
grid-template-columns: 1fr 1fr;
gap: 12px;
padding: 16px;
}
}
/* Even wider: three columns with sidebar */
@container popup (min-width: 520px) {
.popup-grid {
grid-template-columns: 180px 1fr 1fr;
}
}<!-- popup.html -->
<body>
<div class="popup-root">
<header class="popup-header">
<h1>My Extension</h1>
</header>
<main class="popup-grid">
<div class="card">Feature A</div>
<div class="card">Feature B</div>
<div class="card">Feature C</div>
</main>
</div>
</body>Container queries are supported in Chrome 105+, which covers the vast majority of extension users in 2026. You can use them without a polyfill.
Tip: Wrap nested components in their own containers too. A
cardcomponent that is a container can adapt its internal layout independently of the popup’s overall size.
Responsive Sidepanel Layouts
The sidepanel is the most demanding surface to design for because users actively resize it. Think of it like a responsive web page in a narrow column — layouts need to reflow, not just scale down.
Fluid Typography and Spacing
Use clamp() to make type and spacing scale smoothly between sidepanel size extremes:
/* sidepanel.css */
:root {
--text-base: clamp(13px, 1.5vw, 16px);
--space-sm: clamp(6px, 1vw, 10px);
--space-md: clamp(10px, 2vw, 18px);
}
body {
font-size: var(--text-base);
padding: var(--space-md);
}Adaptive Navigation
At narrow widths, a full sidebar nav collapses into an icon-only rail. At wider widths, it shows labels.
.nav {
container-type: inline-size;
container-name: sidepanel-nav;
}
.nav-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
}
.nav-label {
display: none;
white-space: nowrap;
}
@container sidepanel-nav (min-width: 200px) {
.nav-label {
display: inline;
}
}// sidepanel.js — observe panel resize and update state
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const width = entry.contentRect.width;
document.body.dataset.panelSize = width < 300 ? 'narrow' : 'wide';
}
});
resizeObserver.observe(document.body);Using a data- attribute on body alongside CSS container queries gives you both CSS-driven and JS-driven adaptation in sync.
Content Script Overlay Positioning
Injected UI is the trickiest surface. You are a guest in someone else’s page and you need to stay out of the way while remaining accessible.
Shadow DOM for Style Isolation
Always wrap injected UI in a Shadow DOM to prevent the host page’s CSS from leaking in:
// content-script.js
function createOverlay() {
const host = document.createElement('div');
host.id = 'my-extension-host';
// Position the host element
Object.assign(host.style, {
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: '2147483647', // Max z-index
width: '0',
height: '0',
});
document.body.appendChild(host);
// Attach shadow root
const shadow = host.attachShadow({ mode: 'closed' });
// Inject styles into shadow root
const style = document.createElement('style');
style.textContent = getOverlayStyles();
shadow.appendChild(style);
const panel = document.createElement('div');
panel.className = 'overlay-panel';
shadow.appendChild(panel);
return { shadow, panel };
}
function getOverlayStyles() {
return `
.overlay-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 320px;
max-height: 480px;
background: var(--overlay-bg, #ffffff);
color: var(--overlay-text, #1a1a1a);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
}
`;
}Shadow DOM means your styles are completely isolated. The host page cannot break your layout, and your styles cannot accidentally override the host page.
Avoiding Layout Conflicts
Some pages use transform or overflow: hidden on ancestors, which breaks position: fixed. Detect this and fall back gracefully:
function hasTransformAncestor(el) {
let node = el.parentElement;
while (node && node !== document.documentElement) {
const style = window.getComputedStyle(node);
if (style.transform !== 'none' || style.willChange === 'transform') {
return true;
}
node = node.parentElement;
}
return false;
}
// If fixed positioning is broken, fall back to absolute on document.body
const useFixed = !hasTransformAncestor(document.body);
host.style.position = useFixed ? 'fixed' : 'absolute';Dark Mode and Theme Adaptation
Extensions must respect the user’s system color scheme. Hardcoding light colors alienates dark mode users and looks jarring on OLED displays.
CSS Custom Properties with prefers-color-scheme
/* shared theme tokens */
:root {
--color-bg: #ffffff;
--color-surface: #f4f4f5;
--color-text-primary: #18181b;
--color-text-secondary: #71717a;
--color-accent: #4f46e5;
--color-border: #e4e4e7;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #18181b;
--color-surface: #27272a;
--color-text-primary: #fafafa;
--color-text-secondary: #a1a1aa;
--color-accent: #818cf8;
--color-border: #3f3f46;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.4);
}
}For content scripts injected into Shadow DOM, prefers-color-scheme still works inside the shadow root — no extra configuration needed.
Syncing with Chrome’s Theme
You can also read Chrome’s current color scheme via the chrome.action API or by checking window.matchMedia:
// background.js or popup.js
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
function applyTheme(isDark) {
document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
}
applyTheme(prefersDark.matches);
prefersDark.addEventListener('change', e => applyTheme(e.matches));Then in CSS:
[data-theme="dark"] {
--color-bg: #18181b;
/* ...rest of dark tokens */
}This approach lets you override the automatic detection with a user preference toggle while still defaulting to the system setting.
Tip: Store the user’s explicit theme preference in
chrome.storage.syncso it persists across devices and browser sessions.
High-DPI and Icon Sharpness
Blurry icons are an instant sign of a low-quality extension. Use SVG for all UI icons — they scale infinitely without blurring.
For raster images (screenshots, previews), use srcset or the CSS image-set() function:
.hero-image {
background-image: image-set(
url('/images/preview.png') 1x,
url('/images/preview@2x.png') 2x,
url('/images/preview@3x.png') 3x
);
background-size: contain;
}For extension toolbar icons, always provide all four sizes in your manifest:
{
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}Testing Across Screen Sizes
Adaptive UI is only as good as your testing coverage. Here is a practical testing workflow:
1. DevTools for Popups
Right-click your popup and select “Inspect”. Chrome DevTools opens with the popup as the document. You can resize the popup window manually while DevTools is open to test container queries firing at different widths.
2. Simulate Sidepanel Widths
Open your sidepanel and use DevTools to resize the panel by dragging its edge. Watch for layout breaks at 250px (minimum Chrome allows), 350px, and 500px.
3. Test on Real Sites for Content Scripts
Inject your content script on at least these categories of pages:
- Dark-background sites (GitHub, Vercel dashboard)
- Sites with large base font sizes (some news sites set
font-size: 20px) - Single-page apps with aggressive z-index stacking (Gmail, Notion)
- Sites using CSS resets that strip all inherited styles
4. Automated Visual Regression
Use Puppeteer to capture screenshots of your popup at multiple sizes as part of CI:
// test/visual-regression.js
const puppeteer = require('puppeteer');
const POPUP_WIDTHS = [280, 360, 480, 600];
(async () => {
const browser = await puppeteer.launch({
args: [`--load-extension=./dist`, '--disable-extensions-except=./dist']
});
for (const width of POPUP_WIDTHS) {
const page = await browser.newPage();
await page.setViewport({ width, height: 600 });
await page.goto(`chrome-extension://${EXT_ID}/popup.html`);
await page.screenshot({ path: `screenshots/popup-${width}.png` });
}
await browser.close();
})();Compare screenshots between releases to catch visual regressions before they reach users.
Putting It All Together
Building an adaptive extension UI comes down to a few key principles:
- Use container queries instead of media queries for popup and sidepanel layout decisions — they respond to the element, not the viewport.
- Isolate content script styles with Shadow DOM so the host page cannot interfere with your UI.
- Design with CSS custom properties from day one so theme switching (light/dark, custom accent colors) is a single variable change.
- Use SVG for icons and provide multi-resolution rasters for any bitmap images.
- Test on real diverse pages — your content script will encounter things your local test pages never will.
The Chrome extension platform is evolving fast. Sidepanels, offscreen documents, and expanded popup capabilities mean extensions are becoming first-class UI surfaces, not afterthoughts. Investing in adaptive UI now means your extension works beautifully for every user, on every setup — and that is a real competitive advantage in the Chrome Web Store.
Building an extension and want it reviewed before publishing? Check out ExtensionBooster for expert Chrome extension code reviews and store listing optimization.
Share this article
Build better extensions with free tools
Icon generator, MV3 converter, review exporter, and more — no signup needed.
Related Articles
I Built the Same Chrome Extension With 5 Different Frameworks. Here's What Actually Happened.
WXT vs Plasmo vs CRXJS vs Extension.js vs Bedframe. Real benchmarks, honest opinions, and the framework with 12K stars that's quietly dying.
5 Best Email Marketing Services to Grow Your Chrome Extension (2026)
Compare the top email marketing platforms for SaaS and Chrome extension developers. MailerLite, Mailchimp, Brevo, ActiveCampaign, and Drip reviewed.
15 Best Practices to Build a Browser Extension That Users Love (2026 Guide)
Master browser extension development in 2026. Manifest V3, security, performance, and UX best practices to build extensions users love.