[DevTools] Add React Element pane to browser Elements panel (#35240)
Sebastian "Sebbie" Silbermann committed
Jan 15, 2026 at 13:24 UTC
fae15df40ef878fda68756176d501a4cb83cc9bc
7 files changed
+131
-3
packages/react-devtools-extensions/src/main/elementSelection.js
+1
-1
@@ -32,7 +32,7 @@ export function setReactSelectionFromBrowser(bridge) {
32
return;
33
}
34
35
- // Remember to sync the selection next time we show Components tab.
35
+ // Remember to sync the selection next time we show inspected element
36
bridge.send('syncSelectionFromBuiltinElementsPanel');
37
}
38
},
packages/react-devtools-extensions/src/main/index.js
+49
@@ -206,6 +206,7 @@ function createBridgeAndStore() {
206
bridge,
207
browserTheme: getBrowserTheme(),
208
componentsPortalContainer,
209
+ inspectedElementPortalContainer,
210
profilerPortalContainer,
211
editorPortalContainer,
212
currentSelectedSource,
@@ -278,6 +279,51 @@ function createComponentsPanel() {
279
);
280
}
281
282
+function createElementsInspectPanel() {
283
+ if (inspectedElementPortalContainer) {
284
+ // Panel is created and user opened it at least once
285
+ ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
286
+ render();
287
+
288
+ return;
289
+ }
290
+
291
+ if (inspectedElementPane) {
292
+ // Panel is created, but wasn't opened yet, so no document is present for it
293
+ return;
294
+ }
295
+
296
+ const elementsPanel = chrome.devtools.panels.elements;
297
+ if (__IS_FIREFOX__ || !elementsPanel || !elementsPanel.createSidebarPane) {
298
+ // Firefox will not pass the window to the onShown listener despite setPage
299
+ // being called.
300
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=2010549
301
+
302
+ // May not be supported in some browsers.
303
+ // See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools/panels/ElementsPanel/createSidebarPane#browser_compatibility
304
+ return;
305
+ }
306
+
307
+ elementsPanel.createSidebarPane('React Element ⚛', createdPane => {
308
+ inspectedElementPane = createdPane;
309
+
310
+ createdPane.setPage('panel.html');
311
+ createdPane.setHeight('75px');
312
+
313
+ createdPane.onShown.addListener(portal => {
314
+ inspectedElementPortalContainer = portal.container;
315
+ if (inspectedElementPortalContainer != null && render) {
316
+ ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
317
+
318
+ render();
319
+ portal.injectStyles(cloneStyleTags);
320
+
321
+ logEvent({event_name: 'selected-inspected-element-pane'});
322
+ }
323
+ });
324
+ });
325
+}
326
+
327
function createProfilerPanel() {
328
if (profilerPortalContainer) {
329
// Panel is created and user opened it at least once
@@ -508,6 +554,7 @@ function mountReactDevTools() {
554
createComponentsPanel();
555
createProfilerPanel();
556
createSourcesEditorPanel();
557
+ createElementsInspectPanel();
558
// Suspense Tab is created via the hook
559
// TODO(enableSuspenseTab): Create eagerly once Suspense tab is stable
560
}
@@ -556,10 +603,12 @@ let componentsPanel = null;
603
let profilerPanel = null;
604
let suspensePanel = null;
605
let editorPane = null;
606
+let inspectedElementPane = null;
607
let componentsPortalContainer = null;
608
let profilerPortalContainer = null;
609
let suspensePortalContainer = null;
610
let editorPortalContainer = null;
611
+let inspectedElementPortalContainer = null;
612
613
let mostRecentOverrideTab = null;
614
let render = null;
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css
+8
@@ -77,3 +77,11 @@
77
padding: 0.25rem;
78
color: var(--color-console-error-icon);
79
}
80
+
81
+.VRule {
82
+ height: 20px;
83
+ width: 1px;
84
+ flex: 0 0 1px;
85
+ margin: 0 0.5rem;
86
+ background-color: var(--color-border);
87
+}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+13
-2
@@ -34,13 +34,17 @@ import useEditorURL from '../useEditorURL';
34
import styles from './InspectedElement.css';
35
import Tooltip from './reach-ui/tooltip';
36
37
-export type Props = {};
37
+export type Props = {
38
+ actionButtons?: React.Node,
39
+};
40
41
// TODO Make edits and deletes also use transition API!
42
43
const noSourcePromise = Promise.resolve(null);
44
43
-export default function InspectedElementWrapper(_: Props): React.Node {
45
+export default function InspectedElementWrapper({
46
+ actionButtons,
47
+}: Props): React.Node {
48
const {inspectedElementID} = useContext(TreeStateContext);
49
const bridge = useContext(BridgeContext);
50
const store = useContext(StoreContext);
@@ -305,6 +309,13 @@ export default function InspectedElementWrapper(_: Props): React.Node {
309
symbolicatedSourcePromise={symbolicatedSourcePromise}
310
/>
311
)}
312
+
313
+ {actionButtons && (
314
+ <>
315
+ <div className={styles.VRule} />
316
+ {actionButtons}
317
+ </>
318
+ )}
319
</div>
320
321
{inspectedElement === null && (
packages/react-devtools-shared/src/devtools/views/DevTools.js
+11
@@ -26,6 +26,7 @@ import Profiler from './Profiler/Profiler';
26
import SuspenseTab from './SuspenseTab/SuspenseTab';
27
import TabBar from './TabBar';
28
import EditorPane from './Editor/EditorPane';
29
+import InspectedElementPane from './InspectedElement/InspectedElementPane';
30
import {SettingsContextController} from './Settings/SettingsContext';
31
import {TreeContextController} from './Components/TreeContext';
32
import ViewElementSourceContext from './Components/ViewElementSourceContext';
@@ -100,6 +101,7 @@ export type Props = {
101
// The root <DevTools> app is rendered in the top-level extension window,
102
// but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
103
componentsPortalContainer?: Element,
104
+ inspectedElementPortalContainer?: Element,
105
profilerPortalContainer?: Element,
106
suspensePortalContainer?: Element,
107
editorPortalContainer?: Element,
@@ -155,6 +157,7 @@ export default function DevTools({
157
canViewElementSourceFunction,
158
componentsPortalContainer,
159
editorPortalContainer,
160
+ inspectedElementPortalContainer,
161
profilerPortalContainer,
162
suspensePortalContainer,
163
currentSelectedSource,
@@ -379,6 +382,14 @@ export default function DevTools({
382
portalContainer={editorPortalContainer}
383
/>
384
) : null}
385
+ {inspectedElementPortalContainer ? (
386
+ <InspectedElementPane
387
+ selectedSource={currentSelectedSource}
388
+ portalContainer={
389
+ inspectedElementPortalContainer
390
+ }
391
+ />
392
+ ) : null}
393
</ThemeProvider>
394
</SuspenseTreeContextController>
395
</InspectedElementContextController>
packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.css
new
+15
@@ -0,0 +1,15 @@
1
+.InspectedElementPane, .InspectedElementPane * {
2
+ box-sizing: border-box;
3
+ -webkit-font-smoothing: var(--font-smoothing);
4
+}
5
+
6
+.InspectedElementPane {
7
+ position: relative;
8
+ width: 100%;
9
+ height: 100%;
10
+ display: flex;
11
+ flex-direction: row;
12
+ background-color: var(--color-background);
13
+ color: var(--color-text);
14
+ font-family: var(--font-family-sans);
15
+}
packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.js
new
+34
@@ -0,0 +1,34 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import * as React from 'react';
11
+import {useContext} from 'react';
12
+
13
+import portaledContent from 'react-devtools-shared/src/devtools/views/portaledContent';
14
+import {OptionsContext} from 'react-devtools-shared/src/devtools/views/context';
15
+import InspectedElement from 'react-devtools-shared/src/devtools/views/Components/InspectedElement';
16
+import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
17
+import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
18
+import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
19
+import styles from './InspectedElementPane.css';
20
+
21
+function InspectedElementPane() {
22
+ const {hideSettings} = useContext(OptionsContext);
23
+ return (
24
+ <SettingsModalContextController>
25
+ <div className={styles.InspectedElementPane}>
26
+ <InspectedElement
27
+ actionButtons={!hideSettings && <SettingsModalContextToggle />}
28
+ />
29
+ <SettingsModal />
30
+ </div>
31
+ </SettingsModalContextController>
32
+ );
33
+}
34
+export default (portaledContent(InspectedElementPane): component());