Build a Responsive Design Tester Chrome Extension: Preview All Screen Sizes

30 min read

Build a Responsive Design Tester Chrome Extension: Preview All Screen Sizes

Build a Responsive Design Tester Chrome Extension: Preview All Screen Sizes

Responsive web design is no longer optional—it’s essential. With users accessing websites from smartphones, tablets, laptops, and large desktop monitors, developers need tools that help them test designs across all these viewports quickly and efficiently. While browser developer tools offer responsive design mode, a dedicated Chrome extension can provide a more streamlined, customizable experience that fits directly into your workflow.

In this comprehensive guide, we’ll walk you through building a responsive design tester Chrome extension from scratch. You’ll learn how to create an extension that lets users preview their websites at multiple screen sizes simultaneously, save custom viewport configurations, and switch between device presets with a single click.


Why Build a Responsive Design Tester Extension?

The demand for responsive design testing tools continues to grow as web development becomes more complex. Here’s why building a responsive design tester Chrome extension is a valuable project:

Market Demand

Developers and designers spend significant time testing responsive layouts. According to industry surveys, up to 30% of development time is dedicated to cross-browser and cross-device compatibility testing. A well-designed responsive tester extension can dramatically reduce this time investment.

Problem with Existing Solutions

While Chrome DevTools provides responsive design mode, it has limitations:

  • Single viewport at a time: You can only view one screen size at once
  • Limited presets: The default device list is limited and not customizable
  • No side-by-side comparison: Comparing designs across devices requires manual switching
  • No saving of custom configurations: You cannot save your preferred viewport sizes

A dedicated Chrome extension can solve these pain points by offering:

  • Multiple viewport previews in a single interface
  • Custom device presets that you define
  • Quick-switch capabilities between saved configurations
  • Persistent settings that remember your preferences

Learning Opportunities

Building this extension teaches you valuable skills that apply to many other Chrome extension projects:

  • Working with iframe elements for content embedding
  • Managing popup windows and chrome.storage for persistence
  • Implementing keyboard shortcuts for power users
  • Creating dynamic UI that responds to user interactions

Project Architecture

Before writing code, let’s understand the architecture of our responsive design tester extension.

Components Overview

Our extension will consist of several key files:

  1. manifest.json - Extension configuration and permissions
  2. popup.html - The main user interface
  3. popup.js - Application logic for the popup
  4. popup.css - Styling for the interface
  5. background.js - Service worker for extension lifecycle
  6. content.js - Script that runs on web pages

Key Features

We’ll implement these essential features:

  • Device presets: Pre-configured viewports for common devices (iPhone, iPad, Android phones, laptops, desktops)
  • Custom viewport sizes: Ability to define custom width and height
  • Multiple preview mode: View up to four device previews simultaneously
  • Orientation toggle: Switch between portrait and landscape modes
  • Refresh control: Option to auto-refresh when resizing
  • Save configurations: Remember custom presets using chrome.storage
  • URL sync: Apply the current tab’s URL to all previews

Step 1: Setting Up the Manifest

Every Chrome extension starts with the manifest file. For our responsive design tester, we need specific permissions to interact with tabs and storage.

{
  "manifest_version": 3,
  "name": "Responsive Design Tester",
  "version": "1.0.0",
  "description": "Preview your website across multiple screen sizes at once. Test responsive designs effortlessly with device presets and custom viewports.",
  "permissions": [
    "storage",
    "tabs",
    "activeTab"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

Key permission explanations:

  • storage: Allows saving user preferences and custom viewport configurations
  • tabs: Enables reading the current tab’s URL and information
  • activeTab: Provides access to the active tab when the popup is open
  • host_permissions: Grants permission to load URLs in iframes for preview

Step 2: Building the Popup Interface

The popup is the main interaction point for users. Let’s create a clean, intuitive interface.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Responsive Design Tester</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="container">
    <header>
      <h1>Responsive Tester</h1>
      <p class="current-url" id="currentUrl">Loading...</p>
    </header>

    <section class="controls">
      <div class="device-presets">
        <h3>Device Presets</h3>
        <div class="preset-buttons" id="presetButtons">
          <!-- Preset buttons will be injected here -->
        </div>
      </div>

      <div class="custom-viewport">
        <h3>Custom Viewport</h3>
        <div class="viewport-inputs">
          <label>
            Width (px)
            <input type="number" id="customWidth" value="375" min="200" max="3840">
          </label>
          <label>
            Height (px)
            <input type="number" id="customHeight" value="667" min="200" max="2160">
          </label>
          <button id="applyCustom" class="btn primary">Apply</button>
        </div>
      </div>

      <div class="options">
        <label class="checkbox-label">
          <input type="checkbox" id="autoRefresh" checked>
          Auto-refresh on resize
        </label>
        <label class="checkbox-label">
          <input type="checkbox" id="showDeviceFrame" checked>
          Show device frame
        </label>
      </div>
    </section>

    <section class="preview-controls">
      <button id="openPreview" class="btn full-width">Open Multi-Preview</button>
    </section>

    <section class="saved-presets">
      <h3>Saved Configurations</h3>
      <div id="savedList" class="saved-list"></div>
      <button id="saveCurrent" class="btn secondary">Save Current</button>
    </section>
  </div>

  <script src="popup.js"></script>
</body>
</html>

The interface includes sections for device presets, custom viewport input, options, and saved configurations. This layout provides quick access to all features while remaining clean and organized.


Step 3: Styling the Extension

Good design is crucial for developer tools. Let’s create a clean, professional look:

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
  width: 360px;
  min-height: 500px;
  background: #f5f5f5;
  color: #333;
}

.container {
  padding: 16px;
}

header {
  text-align: center;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid #e0e0e0;
}

header h1 {
  font-size: 18px;
  font-weight: 600;
  color: #1a73e8;
  margin-bottom: 4px;
}

.current-url {
  font-size: 11px;
  color: #666;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

section {
  margin-bottom: 16px;
}

h3 {
  font-size: 12px;
  text-transform: uppercase;
  color: #666;
  margin-bottom: 8px;
  letter-spacing: 0.5px;
}

.preset-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.preset-btn {
  padding: 6px 10px;
  font-size: 11px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
  cursor: pointer;
  transition: all 0.2s ease;
}

.preset-btn:hover {
  background: #e8f0fe;
  border-color: #1a73e8;
}

.preset-btn.active {
  background: #1a73e8;
  color: white;
  border-color: #1a73e8;
}

.viewport-inputs {
  display: flex;
  gap: 8px;
  align-items: flex-end;
}

.viewport-inputs label {
  flex: 1;
  font-size: 11px;
}

.viewport-inputs input {
  width: 100%;
  padding: 6px 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 13px;
  margin-top: 4px;
}

.btn {
  padding: 8px 12px;
  border: none;
  border-radius: 4px;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.2s ease;
}

.btn.primary {
  background: #1a73e8;
  color: white;
}

.btn.primary:hover {
  background: #1557b0;
}

.btn.secondary {
  background: #e8f0fe;
  color: #1a73e8;
  width: 100%;
}

.btn.full-width {
  width: 100%;
  background: #1a73e8;
  color: white;
  padding: 12px;
  font-size: 14px;
}

.btn.full-width:hover {
  background: #1557b0;
}

.checkbox-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  margin-bottom: 6px;
  cursor: pointer;
}

.checkbox-label input {
  cursor: pointer;
}

.saved-list {
  margin-bottom: 8px;
  max-height: 100px;
  overflow-y: auto;
}

.saved-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 8px;
  background: white;
  border: 1px solid #eee;
  border-radius: 4px;
  margin-bottom: 4px;
  font-size: 11px;
}

.saved-item span {
  color: #666;
}

.delete-btn {
  background: none;
  border: none;
  color: #dc3545;
  cursor: pointer;
  font-size: 14px;
  padding: 2px 6px;
}

This styling creates a modern, clean interface that matches Chrome’s Material Design guidelines. The layout uses flexbox for responsive positioning and clear visual hierarchy.


Step 4: Implementing the Popup Logic

Now let’s implement the core functionality in popup.js:

// Device presets with common viewport sizes
const devicePresets = [
  { name: 'iPhone SE', width: 375, height: 667 },
  { name: 'iPhone 14', width: 390, height: 844 },
  { name: 'iPhone 14 Pro Max', width: 430, height: 932 },
  { name: 'iPad Mini', width: 768, height: 1024 },
  { name: 'iPad Pro', width: 1024, height: 1366 },
  { name: 'Android', width: 360, height: 800 },
  { name: 'Laptop', width: 1366, height: 768 },
  { name: 'Desktop', width: 1920, height: 1080 }
];

// State
let currentUrl = '';
let currentWidth = 375;
let currentHeight = 667;

// Initialize
document.addEventListener('DOMContentLoaded', async () => {
  await getCurrentTab();
  loadPresets();
  loadSavedConfigs();
  loadOptions();
  
  // Event listeners
  document.getElementById('applyCustom').addEventListener('click', applyCustomViewport);
  document.getElementById('openPreview').addEventListener('click', openMultiPreview);
  document.getElementById('saveCurrent').addEventListener('click', saveCurrentConfig);
  document.getElementById('autoRefresh').addEventListener('change', saveOptions);
  document.getElementById('showDeviceFrame').addEventListener('change', saveOptions);
});

// Get current tab URL
async function getCurrentTab() {
  try {
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    if (tab && tab.url) {
      currentUrl = tab.url;
      document.getElementById('currentUrl').textContent = currentUrl;
    }
  } catch (error) {
    console.error('Error getting tab:', error);
    document.getElementById('currentUrl').textContent = 'Unable to get URL';
  }
}

// Render preset buttons
function loadPresets() {
  const container = document.getElementById('presetButtons');
  container.innerHTML = '';
  
  devicePresets.forEach(device => {
    const btn = document.createElement('button');
    btn.className = 'preset-btn';
    btn.textContent = `${device.name}`;
    btn.dataset.width = device.width;
    btn.dataset.height = device.height;
    
    btn.addEventListener('click', () => {
      selectPreset(device, btn);
    });
    
    container.appendChild(btn);
  });
}

// Handle preset selection
function selectPreset(device, btn) {
  // Update active state
  document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
  btn.classList.add('active');
  
  currentWidth = device.width;
  currentHeight = device.height;
  
  // Update inputs
  document.getElementById('customWidth').value = device.width;
  document.getElementById('customHeight').value = device.height;
  
  // Open preview with this size
  openPreviewWindow(device.width, device.height);
}

// Apply custom viewport
function applyCustomViewport() {
  const width = parseInt(document.getElementById('customWidth').value);
  const height = parseInt(document.getElementById('customHeight').value);
  
  currentWidth = width;
  currentHeight = height;
  
  // Clear preset selection
  document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
  
  openPreviewWindow(width, height);
}

// Open preview window
function openPreviewWindow(width, height) {
  const autoRefresh = document.getElementById('autoRefresh').checked;
  const showFrame = document.getElementById('showDeviceFrame').checked;
  
  const params = new URLSearchParams({
    url: currentUrl,
    width: width,
    height: height,
    autoRefresh: autoRefresh,
    frame: showFrame
  });
  
  chrome.windows.create({
    url: `preview.html?${params.toString()}`,
    type: 'popup',
    width: width + 100,
    height: height + 150
  });
}

// Open multi-preview mode
async function openMultiPreview() {
  const devices = devicePresets.slice(0, 4); // Take first 4 devices
  
  const params = new URLSearchParams({
    url: currentUrl,
    devices: JSON.stringify(devices)
  });
  
  chrome.windows.create({
    url: `multiview.html?${params.toString()}`,
    type: 'normal',
    width: 1400,
    height: 900
  });
}

// Save current configuration
async function saveCurrentConfig() {
  const name = prompt('Enter a name for this configuration:');
  if (!name) return;
  
  const config = {
    name,
    width: currentWidth,
    height: currentHeight,
    timestamp: Date.now()
  };
  
  const saved = await chrome.storage.local.get(['savedConfigs']) || [];
  saved.savedConfigs = saved.savedConfigs || [];
  saved.savedConfigs.push(config);
  
  await chrome.storage.local.set({ savedConfigs: saved.savedConfigs });
  loadSavedConfigs();
}

// Load saved configurations
async function loadSavedConfigs() {
  const result = await chrome.storage.local.get(['savedConfigs']);
  const saved = result.savedConfigs || [];
  
  const container = document.getElementById('savedList');
  container.innerHTML = '';
  
  saved.forEach((config, index) => {
    const item = document.createElement('div');
    item.className = 'saved-item';
    item.innerHTML = `
      <span>${config.name} (${config.width}x${config.height})</span>
      <button class="delete-btn" data-index="${index}">×</button>
    `;
    
    item.querySelector('.delete-btn').addEventListener('click', () => deleteConfig(index));
    container.appendChild(item);
  });
}

// Delete configuration
async function deleteConfig(index) {
  const result = await chrome.storage.local.get(['savedConfigs']);
  let saved = result.savedConfigs || [];
  saved.splice(index, 1);
  await chrome.storage.local.set({ savedConfigs: saved });
  loadSavedConfigs();
}

// Load and save options
async function loadOptions() {
  const result = await chrome.storage.local.get(['options']);
  if (result.options) {
    document.getElementById('autoRefresh').checked = result.options.autoRefresh;
    document.getElementById('showDeviceFrame').checked = result.options.showDeviceFrame;
  }
}

async function saveOptions() {
  const options = {
    autoRefresh: document.getElementById('autoRefresh').checked,
    showDeviceFrame: document.getElementById('showDeviceFrame').checked
  };
  await chrome.storage.local.set({ options });
}

This JavaScript handles all the popup interactions, including preset selection, custom viewport input, saving configurations, and opening preview windows. It uses chrome.storage for persistence and chrome.windows for creating preview interfaces.


Step 5: Creating the Preview Window

The preview window displays the website in an iframe with the selected viewport:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Responsive Preview</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 20px;
      background: #f0f0f0;
      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
    }
    .toolbar {
      display: flex;
      gap: 12px;
      margin-bottom: 16px;
      align-items: center;
    }
    .toolbar button {
      padding: 8px 16px;
      background: #1a73e8;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .toolbar button:hover { background: #1557b0; }
    .viewport-info {
      font-size: 14px;
      color: #333;
      background: white;
      padding: 8px 16px;
      border-radius: 4px;
    }
    .preview-container {
      background: white;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      overflow: hidden;
    }
    iframe {
      border: none;
      display: block;
    }
    .device-frame {
      border: 3px solid #333;
      border-radius: 20px;
      padding: 10px;
      background: #333;
    }
    .device-frame iframe {
      border-radius: 4px;
    }
  </style>
</head>
<body>
  <div class="toolbar">
    <div class="viewport-info" id="viewportInfo"></div>
    <button id="refreshBtn">Refresh</button>
    <button id="toggleFrame">Toggle Frame</button>
    <button id="rotateBtn">Rotate</button>
  </div>
  
  <div class="preview-container" id="previewContainer">
    <div class="device-frame" id="deviceFrame" style="display: none;">
      <iframe id="previewFrame" width="" height=""></iframe>
    </div>
    <iframe id="previewFrameNoFrame" style="display: none;"></iframe>
  </div>

  <script src="preview.js"></script>
</body>
</html>
// preview.js
const params = new URLSearchParams(window.location.search);
const url = params.get('url');
let width = parseInt(params.get('width'));
let height = parseInt(params.get('height'));
const autoRefresh = params.get('autoRefresh') === 'true';
const showFrame = params.get('frame') === 'true';

let isLandscape = false;

function init() {
  updateViewport();
  document.getElementById('viewportInfo').textContent = `${width} × ${height}`;
  
  document.getElementById('refreshBtn').addEventListener('click', refreshPreview);
  document.getElementById('toggleFrame').addEventListener('click', toggleFrame);
  document.getElementById('rotateBtn').addEventListener('click', rotate);
  
  if (autoRefresh) {
    window.addEventListener('resize', debounce(refreshPreview, 500));
  }
}

function updateViewport() {
  const frame = document.getElementById('deviceFrame');
  const noFrame = document.getElementById('previewFrameNoFrame');
  const previewFrame = document.getElementById('previewFrame');
  
  if (showFrame) {
    frame.style.display = 'block';
    noFrame.style.display = 'none';
    previewFrame.width = isLandscape ? height : width;
    previewFrame.height = isLandscape ? width : height;
    previewFrame.src = url;
  } else {
    frame.style.display = 'none';
    noFrame.style.display = 'block';
    noFrame.style.width = isLandscape ? height + 'px' : width + 'px';
    noFrame.style.height = isLandscape ? width + 'px' : height + 'px';
    noFrame.src = url;
  }
}

function refreshPreview() {
  const frame = document.getElementById('previewFrame');
  const noFrame = document.getElementById('previewFrameNoFrame');
  frame.src = frame.src;
  noFrame.src = noFrame.src;
}

function toggleFrame() {
  const newShowFrame = !showFrame;
  const params = new URLSearchParams(window.location.search);
  params.set('frame', newShowFrame);
  window.location.search = params.toString();
}

function rotate() {
  isLandscape = !isLandscape;
  [width, height] = [height, width];
  document.getElementById('viewportInfo').textContent = `${width} × ${height}`;
  updateViewport();
}

function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

init();

Step 6: Creating the Multi-View Preview

For developers who want to see multiple viewports at once, we create a multiview page:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Multi-Device Preview</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      padding: 20px;
      background: #1a1a1a;
      font-family: -apple-system, sans-serif;
    }
    h1 {
      color: white;
      margin-bottom: 20px;
      font-size: 20px;
    }
    .grid {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 20px;
    }
    .viewport {
      background: white;
      border-radius: 8px;
      overflow: hidden;
    }
    .viewport-header {
      background: #333;
      color: white;
      padding: 8px 12px;
      font-size: 12px;
      display: flex;
      justify-content: space-between;
    }
    iframe {
      border: none;
      width: 100%;
      display: block;
    }
  </style>
</head>
<body>
  <h1>Multi-Device Preview</h1>
  <div class="grid" id="grid"></div>
  
  <script>
    const params = new URLSearchParams(window.location.search);
    const url = params.get('url');
    const devices = JSON.parse(params.get('devices') || '[]');
    
    const grid = document.getElementById('grid');
    
    devices.forEach(device => {
      const viewport = document.createElement('div');
      viewport.className = 'viewport';
      viewport.innerHTML = `
        <div class="viewport-header">
          <span>${device.name}</span>
          <span>${device.width} × ${device.height}</span>
        </div>
        <iframe src="${url}" style="height: ${device.height * 0.5}px;"></iframe>
      `;
      grid.appendChild(viewport);
    });
  </script>
</body>
</html>

This multi-view implementation displays up to four device previews in a grid layout, allowing developers to compare different viewport sizes simultaneously.


Step 7: Background Service Worker

The background service worker handles extension lifecycle events:

// background.js

// Handle extension installation
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    // Set default options
    chrome.storage.local.set({
      options: {
        autoRefresh: true,
        showDeviceFrame: true
      },
      savedConfigs: []
    });
    
    console.log('Responsive Design Tester installed!');
  }
});

// Handle messages from popup or content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'getPreviewUrl') {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs[0]) {
        sendResponse({ url: tabs[0].url });
      }
    });
    return true;
  }
});

// Handle keyboard shortcuts (if you want to add them)
chrome.commands.onCommand.addListener((command) => {
  if (command === 'open-responsive-tester') {
    chrome.action.openPopup();
  }
});

Testing Your Extension

Before publishing, thoroughly test your extension:

  1. Load the extension: Open chrome://extensions/, enable Developer mode, click “Load unpacked,” and select your extension folder.

  2. Test device presets: Click each preset button and verify the preview window opens with the correct dimensions.

  3. Test custom viewport: Enter custom width and height values, click Apply, and confirm the preview matches.

  4. Test multi-view: Click “Open Multi-Preview” and verify all four previews load correctly.

  5. Test persistence: Save a custom configuration, reload the extension, and verify it appears in the saved list.

  6. Test options: Toggle auto-refresh and device frame options, reload, and verify they persist.

  7. Test on different sites: Try the extension on various websites to ensure iframes load correctly.


Publishing Your Extension

Once testing is complete, follow these steps to publish:

  1. Create a zip file: Compress your extension folder (excluding .git and other unnecessary files).

  2. Create developer account: If you don’t have one, set up a Google Developer account at the Chrome Web Store Developer Dashboard.

  3. Upload: Submit your zip file and fill in the store listing details:
    • Extension name: “Responsive Design Tester”
    • Description: Include your keywords naturally
    • Screenshots: Show the extension in action
    • Category: Developer Tools
  4. Publish: After review (usually 24-72 hours), your extension will be live.

Conclusion

Building a responsive design tester Chrome extension is an excellent project that teaches fundamental Chrome extension development concepts while creating a genuinely useful tool. Throughout this guide, you’ve learned how to:

  • Set up a Manifest V3 extension with proper permissions
  • Create intuitive popup interfaces with HTML and CSS
  • Implement core functionality with JavaScript
  • Use chrome.storage for persistent configurations
  • Create preview windows with iframes
  • Build multi-view layouts for side-by-side comparison
  • Test and publish your extension

The extension you built today addresses real pain points that developers face daily. With some additional features like screenshot capture, CSS breakpoint detection, or integration with design tools, you could expand this into a full-featured product.

Remember to continue iterating based on user feedback, and don’t hesitate to explore more advanced Chrome extension APIs as you become more comfortable with the platform. Happy coding!

No previous article
No next article