How to Build Custom Chrome DevTools Panels and Extensions (2026)
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:
| Component | Access | Purpose |
|---|---|---|
| DevTools page | DevTools APIs + Extension APIs | Entry point, creates panels/panes |
| Panel HTML | Extension APIs | Your custom UI |
| Service worker | Extension APIs | Background coordination |
| Content scripts | DOM of inspected page | Page 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
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.