Adding an Elements Sidebar Pane
Add a custom sidebar pane to the DevTools Elements panel: panels.elements.createSidebarPane, setExpression and setObject for quick data, setPage for rich UI, reacting to onSelectionChanged and $0, and keeping evaluation safe.
Table of Contents
A design-system team wants every developer to see, for the element they select in DevTools, which design token produced its colour and which component rendered it. A whole custom panel is overkill — developers live in the Elements panel, where the Styles, Computed and Event Listeners panes already sit. DevTools extensions can add a pane right there with chrome.devtools.panels.elements.createSidebarPane, and fill it with data about the currently selected element, $0. The API is small but has three ways to show content, each with trade-offs. This guide uses all three. It belongs to side panel and DevTools interfaces.
How sidebar panes work
From the DevTools page, chrome.devtools.panels.elements.createSidebarPane(title, callback) adds a tab next to Styles and Computed and returns an ExtensionSidebarPane. It can show content three ways: setExpression(expr, rootTitle) evaluates a JavaScript expression in the inspected page and shows the result as an expandable object tree, like the Console; setObject(jsonObject, rootTitle) shows a JSON object you computed yourself; setPage(path) loads an extension HTML page into the pane for fully custom UI. panels.elements.onSelectionChanged fires when the user selects a different element, and inside evaluated expressions $0 refers to the selected element. panels.sources offers the same for the Sources panel.
Step-by-step: a “Design tokens” pane
1. Create the pane from the DevTools page
1// devtools.js
2chrome.devtools.panels.elements.createSidebarPane("Design tokens", (pane) => {
3 pane.setHeight?.("auto");
4 const update = () => pane.setExpression(`(${tokenInfo.toString()})($0)`, "Selected element");
5 chrome.devtools.panels.elements.onSelectionChanged.addListener(update);
6 update();
7});
Execution context: the DevTools page (declared with devtools_page). The pane is created once per DevTools window. Updating on onSelectionChanged keeps it in sync as the user clicks around the DOM tree. setHeight is a no-op in some versions; the pane sizes to its content.
2. Write the expression to run in the page
1// tokens-info.js — serialised into the expression, runs in the inspected page's main world
2function tokenInfo(el) {
3 if (!el || el.nodeType !== 1) return { note: "Select an element" };
4 const cs = getComputedStyle(el);
5 const props = ["color", "background-color", "border-color", "font-size", "padding"];
6 const vars = {};
7 for (const sheet of document.styleSheets) {
8 let rules; try { rules = sheet.cssRules; } catch { continue; } // cross-origin sheets throw
9 for (const r of rules) if (r.selectorText && el.matches(r.selectorText)) {
10 for (const p of props) { const v = r.style.getPropertyValue(p); if (v.includes("var(--")) vars[p] = v.trim(); }
11 }
12 }
13 return {
14 component: el.closest("[data-component]")?.dataset.component ?? null,
15 tokens: vars,
16 computed: Object.fromEntries(props.map((p) => [p, cs.getPropertyValue(p)])),
17 };
18}
Execution context: the inspected page’s main world, via setExpression. Serialising a function with toString() and calling it with $0 keeps the code readable in your source while running it in the page. The result is a plain object, which DevTools renders as an expandable tree. Return small summaries — DOM nodes in the result render as nodes, which is useful, but whole subtrees are noisy.
3. Use setObject when you compute elsewhere
1chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
2 chrome.devtools.inspectedWindow.eval("$0 && $0.dataset.component", async (name) => {
3 const doc = name ? await lookupComponentDocs(name) : null; // from the extension's bundled data
4 pane.setObject(doc ?? { note: "No component" }, name ?? "Component");
5 });
6});
Execution context: the DevTools page. When data comes from the extension (bundled docs, a design-system registry, a server), read the minimum from the page with inspectedWindow.eval, then compute and pass a JSON object to setObject. Objects must be JSON-serialisable.
4. Use setPage for rich, interactive UI
1chrome.devtools.panels.elements.createSidebarPane("A11y", (pane) => {
2 pane.setPage("a11y-pane.html");
3 pane.onShown.addListener((win) => { paneWindow = win; refresh(); });
4 pane.onHidden.addListener(() => { paneWindow = null; });
5 chrome.devtools.panels.elements.onSelectionChanged.addListener(refresh);
6});
7
8function refresh() {
9 if (!paneWindow) return;
10 chrome.devtools.inspectedWindow.eval(`(${a11ySummary.toString()})($0)`, (result) => paneWindow.render(result));
11}
Execution context: the DevTools page and a pane page. setPage loads an extension page into the sidebar; onShown passes its window, so the DevTools page can call functions the pane exposes. Only evaluate when the pane is visible — a pane the user is not looking at should not run page code on every selection. The pane page is an extension page with the extension’s CSP; render data with textContent.
5. Keep evaluation safe and side-effect free
Expressions run in the inspected page with the page’s privileges — the page can also tamper with globals your expression uses. Read only; never modify the page from a sidebar expression unless the user clicks an explicit action. Avoid depending on page globals like Array.prototype being untouched for anything security-relevant, and treat returned values as untrusted data in your pane.
6. Handle frames
$0 refers to the element selected in whichever frame the user is inspecting. inspectedWindow.eval accepts { frameURL } or { useContentScriptContext: true } options; for most sidebar panes, evaluating in the default context with $0 does the right thing because DevTools sets $0 in the frame that owns the selection.
7. Add a Sources sidebar the same way
chrome.devtools.panels.sources.createSidebarPane adds a pane to the Sources panel with the same API — useful for showing build metadata or source-map details next to the debugger.
8. Debounce rapid selection changes
1let pendingEval;
2chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
3 clearTimeout(pendingEval);
4 pendingEval = setTimeout(update, 80);
5});
Execution context: the DevTools page. Holding an arrow key in the Elements tree fires a selection change for every node passed. Evaluating an expression that walks every stylesheet on each of them makes DevTools sluggish. A short debounce evaluates only once the selection settles, which feels instant to the user and keeps the inspected page responsive. For expensive expressions, also cache results per element by tagging $0 with a WeakMap inside the page context, so returning to a previously inspected node is free.
Common mistakes
- Not updating on selection change. The pane shows the first element forever.
- Huge result objects. The tree becomes unusable; summarise.
- Evaluating while hidden. Wasted work on every click.
- Writing to the page from an expression. Inspection must not change what it inspects.
innerHTMLin setPage panes. Page-derived data is untrusted.
Cross-browser variation
- Chrome / Edge:
elements.createSidebarPanewithsetExpression,setObject,setPage,onShown/onHidden;sources.createSidebarPanetoo. - Firefox:
panels.elements.createSidebarPanesupportssetExpressionandsetObject;setPagesupport is more limited — check your target version. - Safari: Web Inspector extensions support custom tabs; sidebar pane support differs — verify before relying on it.
Verification
- Open DevTools, select elements and confirm the pane updates each time.
- Select a text node or comment and confirm the “Select an element” note appears.
- With the setPage pane hidden, confirm no evaluations happen on selection.
- Inspect an element inside an iframe and confirm the pane reflects it.
FAQ
Can the pane appear in the Console or Network panel?
No. Sidebar panes exist for the Elements and Sources panels only.
Can I select an element from my pane?
Yes: chrome.devtools.inspectedWindow.eval("inspect(document.querySelector('…'))") selects it in the Elements panel.
Is a content script needed?
No. setExpression and inspectedWindow.eval run in the page without one.
How do I share code between the expression and my tests?
Keep the function in its own module that exports it, import it into the DevTools page for toString() serialisation, and import the same module in unit tests that run it against a JSDOM element. Avoid closures over outer variables, which are lost when the function is serialised.
Can a pane show data for multiple selected elements?
The Elements panel selects one element at a time, so $0 is always a single node. To compare elements, keep a history of recent selections in your pane and show them side by side.
Related
- Building a custom DevTools panel — full panels.
- Inspecting page state from a DevTools extension —
inspectedWindow.eval. - Connecting a DevTools panel to the service worker — continuous data.
- Side panel and DevTools interfaces — the parent topic.