As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
I remember the first time I tried to build something that lived inside Chrome DevTools. I had this weird idea—a panel that would show me every time a variable changed in my React app. I knew content scripts could touch the page, but I wanted the panel to feel native, like the Elements tab or the Console. So I started digging into the extension APIs, and I found out that the browser opens a whole set of tools just for people like us, who want to make custom debugging gadgets. Let me show you what I learned.
The whole thing begins with a plain JSON file. Your extension needs a manifest, and inside it you declare a "devtools_page". This is just an HTML file that will be loaded when the user opens DevTools. But that page’s real job is to register your custom panels. So you create a small HTML file like devtools.html that doesn’t have much content itself—it just calls chrome.devtools.panels.create. That API takes a name, an icon, and the path to another HTML file that will display your panel’s UI. The permissions are simple: you need "storage" if you want to save settings, "tabs" to communicate with the inspected page, and "activeTab" for one-off access. You don’t need the broad "devtools" permission; the "devtools_page" field is enough.
{
"name": "My Custom Inspector",
"version": "1.0.0",
"devtools_page": "devtools.html",
"permissions": ["storage", "tabs", "activeTab"],
"background": { "scripts": ["background.js"] }
}
In devtools.html you might write just a script that creates the panel:
chrome.devtools.panels.create(
"My Panel",
"icon.png",
"panel.html"
);
Now when you open DevTools, you’ll see a new tab named "My Panel". That’s where your custom interface lives. But getting data from the webpage into that panel requires a little dance.
The inspected page is separate from your DevTools extension. You can’t just throw a script tag into it from the panel. Instead, you use chrome.devtools.inspectedWindow.eval(). This function runs JavaScript code inside the page’s context and returns the result. I use this to grab any global state I care about—like a Redux store snapshot, or a list of all registered service workers. The second parameter is an options object where you can set useContentScriptContext: true if you want the code to run with the same permissions as a content script. That’s helpful for accessing DOM elements that content scripts can see.
function getAppState() {
chrome.devtools.inspectedWindow.eval(
`JSON.stringify(window.__APP_STATE__)`,
{ useContentScriptContext: true },
(result, isException) => {
if (!isException) {
updatePanel(JSON.parse(result));
}
}
);
}
But eval only gives you a one-shot communication. If you want the panel to receive real-time updates from the page—like when a user clicks a button—you need a permanent bridge. That’s where the background script comes in. Open a port from the panel to the background using chrome.runtime.connect. Then in the background script, forward messages to a content script that lives in the inspected tab. The content script can listen for events on the page and send them back. It’s a round trip, but it works reliably.
// panel.js
const port = chrome.runtime.connect({ name: 'panel-background' });
port.onMessage.addListener(msg => displayEvent(msg));
// background.js
chrome.runtime.onConnect.addListener(port => {
port.onMessage.addListener((msg, sender) => {
chrome.tabs.sendMessage(sender.sender.tabId, msg, response => {
port.postMessage(response);
});
});
});
That bridge becomes the backbone for custom network inspectors. The DevTools extension API gives you chrome.devtools.network which can listen for every HTTP request that happens in the inspected page. I use onRequestFinished to catch the response details. The callback gives you a request object with methods to get the response body. I filter for API calls and log them in my panel. But you can go further. There’s also onBeforeRequest which fires before the request is sent. You can modify request headers there. That’s handy if you want to add a debug header to every API call, or block certain resources during testing.
// Listen for completed requests
chrome.devtools.network.onRequestFinished.addListener(request => {
if (request.request.url.includes('/api/')) {
request.getContent(content => {
logApiCall({
url: request.request.url,
method: request.request.method,
status: request.response.status,
body: content
});
});
}
});
// Modify headers before request is sent
chrome.devtools.network.onBeforeRequest.addListener(details => {
details.requestHeaders.push({ name: 'X-Debug', value: 'true' });
return { requestHeaders: details.requestHeaders };
});
Performance profiling inside a custom panel is surprisingly easy. The page can use the standard performance.mark() and performance.measure() methods, and your panel can read them using inspectedWindow.eval. I once built a tool that measured how long each step in a file upload process took. I marked the start and end in the page, then on the panel side I pulled all the measures and rendered a simple horizontal bar chart. No need for the complex performance timeline API; the user’s own markers give you exactly what you need.
// In the page script (injected or existing)
performance.mark('upload-start');
fetch('/upload', { method: 'POST', body: file })
.then(() => performance.mark('upload-end'))
.then(() => performance.measure('upload-time', 'upload-start', 'upload-end'));
// In panel.js
chrome.devtools.inspectedWindow.eval(
`JSON.stringify(performance.getEntriesByType('measure'))`,
result => renderTimeline(JSON.parse(result))
);
DOM inspection panels can go way beyond what the default Elements tab shows. You can query any element in the page, get its computed styles, bounding rectangle, event listeners, even accessibility properties. I built a panel that highlights elements with a specific CSS class and shows their z-index stacking context. The trick is to use inspectedWindow.eval to run a function that returns a plain object with only the data you need. You can’t send DOM nodes directly because they’re not serializable. So you extract the relevant properties.
function inspectElement(selector) {
chrome.devtools.inspectedWindow.eval(
`(function() {
const el = document.querySelector('${selector}');
if (!el) return null;
const rect = el.getBoundingClientRect();
const styles = window.getComputedStyle(el);
return {
tag: el.tagName,
classes: Array.from(el.classList),
id: el.id,
rect: [rect.left, rect.top, rect.width, rect.height],
color: styles.color,
fontSize: styles.fontSize,
zIndex: styles.zIndex
};
})()`,
result => {
if (result) highlightInPage(result);
}
);
}
Sidebar panes are a special kind of panel that lives inside the Elements tab. When you select a DOM node in the Elements panel, a sidebar can show you extra information about that node. I use chrome.devtools.panels.elements.createSidebarPane and then listen for onSelectionChanged. Inside that listener, I evaluate an expression that uses $0—a special DevTools variable that holds the currently selected element. Then I call sidebar.setObject() to display the result as a JSON tree. This is perfect for showing computed data attributes, event listeners, or any custom metadata attached to the element.
chrome.devtools.panels.elements.createSidebarPane("Custom Props", sidebar => {
chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
chrome.devtools.inspectedWindow.eval(
`(function() {
const el = $0;
if (!el) return null;
return {
dataset: Object.assign({}, el.dataset),
attributes: Array.from(el.attributes).map(a => ({name: a.name, value: a.value}))
};
})()`,
result => sidebar.setObject(result || { error: 'No element selected' })
);
});
});
Sometimes the extension APIs aren’t enough. For example, if you want to enable raw network traffic capture or manipulate the DOM tree in ways that the API doesn’t allow, you can connect directly to Chrome DevTools Protocol (CDP) via a WebSocket. You need to know the WebSocket debug URL of the inspected page. You can get that from chrome.debugger API or by starting a remote debugging session. Once you have the URL, you open a WebSocket and send JSON commands like "Network.enable" or "DOM.getDocument". This gives you the same power as the full DevTools frontend. I used this to build a custom profiler that recorded every HTTP request with precise timestamps that the high-level API didn’t expose.
// background.js
const ws = new WebSocket('ws://localhost:9222/devtools/page/PAGE_ID');
ws.onopen = () => {
ws.send(JSON.stringify({ id: 1, method: 'Network.enable' }));
ws.send(JSON.stringify({ id: 2, method: 'DOM.getDocument' }));
};
ws.onmessage = event => {
const msg = JSON.parse(event.data);
if (msg.method === 'Network.requestWillBeSent') {
console.log(msg.params.request.url);
}
};
Finally, a real tool has to feel like part of DevTools, not a foreign popup. Save user preferences to chrome.storage.local so settings persist between sessions. I always store the last active filter, the panel size, and any logs the user chose to keep. Also, listen for theme changes with chrome.devtools.panels.onThemeChanged. When the user switches DevTools from light to dark mode, your panel’s CSS variables should update automatically. I set a data attribute on the body and let CSS handle the rest.
// Save state
chrome.storage.local.set({ filter: 'error', collapsed: true });
// Load state
chrome.storage.local.get(['filter', 'collapsed'], items => {
applyFilter(items.filter);
setCollapsed(items.collapsed);
});
// Theme adaptation
chrome.devtools.panels.onThemeChanged.addListener(theme => {
document.body.dataset.theme = theme; // 'default' or 'dark'
});
Building these panels taught me that the browser gives you a surprising amount of freedom. You are not limited to just injecting content scripts. You can create full-blown debugging environments that integrate with every part of DevTools. The manifest sets the stage, the inspectedWindow API gives you access to the page, the network hook lets you see traffic, and the CDP open the door to the very core of the browser. With message passing, you can build real-time dashboards. With sidebars, you can inspect elements in ways that even the default tools don’t offer. And with persistence and theming, your tool feels like a first-class citizen.
I encourage you to start small. Create a panel that prints the current URL. Then expand it to show something specific to your project, like the number of open WebSocket connections or the current route. Once you taste that power, you will never look at DevTools the same way again. It’s not just a tool—it’s a platform you can extend. And now you have the techniques to do it.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)