How to Build Custom Chrome DevTools Panels and Extensions (2026)

AppBooster Team · · 5 min read
Developer working with Chrome DevTools

Why Build a DevTools Extension?

Chrome DevTools extensions let you add custom panels alongside Elements, Console, and Network — perfect for building framework-specific debuggers, performance monitors, or specialized inspection tools. React DevTools and Redux DevTools are both built this way.

If your extension helps developers debug, analyze, or inspect web applications, a DevTools panel gives you native-feeling integration with Chrome’s developer workflow.


Architecture Overview

A DevTools extension has four interconnected components:

ComponentAccessPurpose
DevTools pageDevTools APIs + Extension APIsEntry point, creates panels/panes
Panel HTMLExtension APIsYour custom UI
Service workerExtension APIsBackground coordination
Content scriptsDOM of inspected pagePage interaction

The DevTools page is loaded each time DevTools opens. It can create panels and sidebar panes, but it cannot directly access the inspected page’s DOM.


Manifest Configuration

{
  "manifest_version": 3,
  "name": "My DevTools Extension",
  "version": "1.0",
  "devtools_page": "devtools.html",
  "permissions": ["scripting"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content-script.js"]
    }
  ]
}

The devtools_page must point to a local HTML file — this is your extension’s entry point into DevTools.


Creating a Custom Panel

devtools.html

<!DOCTYPE html>
<html>
  <body>
    <script src="devtools.js"></script>
  </body>
</html>

devtools.js

chrome.devtools.panels.create(
  'My Debugger',          // Panel title (tab name)
  'icons/panel-icon.png', // 16x16 icon
  'panel.html',           // Panel content page
  (panel) => {
    panel.onShown.addListener((panelWindow) => {
      // Panel is visible — initialize or refresh UI
      panelWindow.document.getElementById('refresh').addEventListener('click', () => {
        panelWindow.postMessage({ action: 'refresh' }, '*');
      });
    });

    panel.onHidden.addListener(() => {
      // Panel hidden — pause expensive operations
    });
  }
);

panel.html

<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: system-ui; padding: 16px; color: #333; }
    table { width: 100%; border-collapse: collapse; }
    th, td { padding: 8px; border-bottom: 1px solid #eee; text-align: left; }
    th { background: #f5f5f5; font-weight: 600; }
    .btn { padding: 6px 12px; background: #4A90D9; color: white; border: none; border-radius: 4px; cursor: pointer; }
  </style>
</head>
<body>
  <h2>Component Inspector</h2>
  <button class="btn" id="refresh">Refresh</button>
  <table id="data-table">
    <thead><tr><th>Name</th><th>Value</th><th>Type</th></tr></thead>
    <tbody></tbody>
  </table>
  <script src="panel.js"></script>
</body>
</html>

Creating Sidebar Panes

Add supplementary panels to the Elements panel sidebar:

// In devtools.js
chrome.devtools.panels.elements.createSidebarPane(
  'Component Props',
  (sidebar) => {
    // Option 1: Display a JSON object
    sidebar.setObject({ loading: true });

    // Option 2: Evaluate an expression in the inspected page
    sidebar.setExpression('document.querySelector("meta[name=description]")?.content');

    // Option 3: Display a custom HTML page
    sidebar.setPage('sidebar.html');
  }
);

Updating Sidebar on Element Selection

chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
  chrome.devtools.inspectedWindow.eval(
    `(function() {
      const el = $0; // Currently selected element
      return {
        tag: el.tagName,
        id: el.id,
        classes: Array.from(el.classList),
        dataset: Object.assign({}, el.dataset),
        computedStyles: {
          display: getComputedStyle(el).display,
          position: getComputedStyle(el).position
        }
      };
    })()`,
    (result, error) => {
      if (!error) {
        sidebar.setObject(result, 'Selected Element');
      }
    }
  );
});

Inspecting the Page

Evaluate JavaScript in the Inspected Page

// Simple evaluation
chrome.devtools.inspectedWindow.eval(
  'document.querySelectorAll("img:not([alt])").length',
  (result, error) => {
    if (!error) {
      console.log(`Found ${result} images without alt text`);
    }
  }
);

// Use DevTools console utilities ($0, $$, inspect, etc.)
chrome.devtools.inspectedWindow.eval(
  'inspect($$("header")[0])'
);

Monitor Network Requests

chrome.devtools.network.onRequestFinished.addListener((request) => {
  if (request.response.status >= 400) {
    console.log(`Failed: ${request.request.url} (${request.response.status})`);
  }

  request.getContent((body, encoding) => {
    if (request.response.content.mimeType === 'application/json') {
      const data = JSON.parse(body);
      // Analyze API responses
    }
  });
});

Inter-Component Communication

DevTools extensions use message passing to coordinate between components:

DevTools Page → Service Worker

// devtools.js
const port = chrome.runtime.connect({ name: 'devtools-page' });
port.postMessage({
  action: 'init',
  tabId: chrome.devtools.inspectedWindow.tabId
});

port.onMessage.addListener((message) => {
  if (message.action === 'dom-updated') {
    // Refresh panel data
  }
});

Service Worker Hub

// service-worker.js
const devtoolsPorts = new Map();

chrome.runtime.onConnect.addListener((port) => {
  if (port.name === 'devtools-page') {
    let tabId = null;

    port.onMessage.addListener((message) => {
      if (message.action === 'init') {
        tabId = message.tabId;
        devtoolsPorts.set(tabId, port);
      }
    });

    port.onDisconnect.addListener(() => {
      if (tabId) devtoolsPorts.delete(tabId);
    });
  }
});

// Forward content script messages to DevTools
chrome.runtime.onMessage.addListener((message, sender) => {
  const port = devtoolsPorts.get(sender.tab?.id);
  if (port) {
    port.postMessage(message);
  }
});

Inject Content Script from DevTools

// devtools.js
chrome.scripting.executeScript({
  target: { tabId: chrome.devtools.inspectedWindow.tabId },
  files: ['content-script.js']
});

Detecting DevTools Open/Close

Track whether DevTools is open for a given tab:

// service-worker.js
const devtoolsOpen = new Set();

chrome.runtime.onConnect.addListener((port) => {
  if (port.name === 'devtools-page') {
    port.onMessage.addListener((msg) => {
      if (msg.tabId) {
        devtoolsOpen.add(msg.tabId);
      }
    });
    port.onDisconnect.addListener(() => {
      // Port closed — DevTools was closed
      // (need tabId tracking for proper cleanup)
    });
  }
});

Important: DevTools pages don’t fire unload events reliably. Use the onDisconnect pattern with a heartbeat:

// devtools.js
const port = chrome.runtime.connect({ name: 'devtools-page' });
setInterval(() => port.postMessage({ type: 'heartbeat' }), 15000);

What’s Next

DevTools extensions open up powerful possibilities for developer tooling. Start with a simple panel, add inspected window evaluation, and gradually build toward a full-featured debugging experience.

Need to grow your extension’s user base? ExtensionBooster helps developers analyze, optimize, and promote their Chrome extensions.

Share this article

Build better extensions with free tools

Icon generator, MV3 converter, review exporter, and more — no signup needed.

Related Articles