Repository: facebook/react
Pull request: [DevTools] Give a distinct color to the root

Stacked on #34654.

The root is special since it represents "Initial Paint" (or a "Transition" when an Activity is selected). This gives it a different color in the timeline as well as gives it an outline that's clickable. Hovering the timeline now shows "Initial Paint" or "Suspense".

Also made the cursor a pointer to invite you to try to click things and some rounded corners.

<img width="1219" height="420" alt="Screenshot 2025-10-02 at 1 26 38 PM" src="https://github.com/user-attachments/assets/12451f93-8917-4f3b-8f01-930129e5fc13" />

<img width="1217" height="419" alt="Screenshot 2025-10-02 at 1 26 54 PM" src="https://github.com/user-attachments/assets/02b5e94c-3fbe-488d-b0f2-225b73578608" />

<img width="1215" height="419" alt="Screenshot 2025-10-02 at 1 27 24 PM" src="https://github.com/user-attachments/assets/c24e8861-e74a-4ccc-8643-ee9d04bef43c" />

<img width="1216" height="419" alt="Screenshot 2025-10-02 at 1 27 10 PM" src="https://github.com/user-attachments/assets/d5cc2b62-fa64-41bf-b485-116b1cd67467" />


Changed files:
filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css
patch: @@ -1,5 +1,12 @@
 .SuspenseRectsContainer {
   padding: .25rem;
+  cursor: pointer;
+  outline: 1px solid var(--color-component-name);
+  border-radius: 0.25rem;
+}
+
+.SuspenseRectsContainer[data-highlighted='true'] {
+  background: var(--color-dimmest);
 }
 
 .SuspenseRectsViewBox {
@@ -28,6 +35,8 @@
   pointer-events: all;
   outline-style: solid;
   outline-width: 1px;
+  border-radius: 0.125rem;
+  cursor: pointer;
 }
 
 .SuspenseRectsScaledRect {
@@ -42,7 +51,7 @@
 
 /* highlight this boundary */
 .SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
-    background-color: var(--color-background-hover);
+  background-color: var(--color-background-hover);
 }
 
 .SuspenseRectsRect[data-highlighted='true'] {

filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js
patch: @@ -295,6 +295,7 @@ const ViewBox = createContext<Rect>((null: any));
 
 function SuspenseRectsContainer(): React$Node {
   const store = useContext(StoreContext);
+  const {inspectedElementID} = useContext(TreeStateContext);
   const treeDispatch = useContext(TreeDispatcherContext);
   const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
   // TODO: This relies on a full re-render of all children when the Suspense tree changes.
@@ -329,8 +330,13 @@ function SuspenseRectsContainer(): React$Node {
     });
   }
 
+  const isRootSelected = roots.includes(inspectedElementID);
+
   return (
-    <div className={styles.SuspenseRectsContainer} onClick={handleClick}>
+    <div
+      className={styles.SuspenseRectsContainer}
+      onClick={handleClick}
+      data-highlighted={isRootSelected}>
       <ViewBox.Provider value={boundingBox}>
         <div
           className={styles.SuspenseRectsViewBox}

filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css
patch: @@ -37,7 +37,7 @@
   padding-right: 0;
 }
 
-.SuspenseScrubberBead, .SuspenseScrubberBeadSelected {
+.SuspenseScrubberBead {
   flex: 1;
   height: 0.5rem;
   background: var(--color-background-selected);
@@ -51,9 +51,11 @@
   background: var(--color-background-selected);
 }
 
+.SuspenseScrubberBeadTransition {
+  background: var(--color-component-name);
+}
+
 .SuspenseScrubberStepHighlight > .SuspenseScrubberBead,
-.SuspenseScrubberStepHighlight > .SuspenseScrubberBeadSelected,
-.SuspenseScrubberStep:hover > .SuspenseScrubberBead,
-.SuspenseScrubberStep:hover > .SuspenseScrubberBeadSelected {
+.SuspenseScrubberStep:hover > .SuspenseScrubberBead {
   height: 0.75rem;
 }

filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js
patch: @@ -14,6 +14,8 @@ import {useRef} from 'react';
 
 import styles from './SuspenseScrubber.css';
 
+import Tooltip from '../Components/reach-ui/tooltip';
+
 export default function SuspenseScrubber({
   min,
   max,
@@ -53,24 +55,38 @@ export default function SuspenseScrubber({
   const steps = [];
   for (let index = min; index <= max; index++) {
     steps.push(
-      <div
+      <Tooltip
         key={index}
-        className={
-          styles.SuspenseScrubberStep +
-          (highlight === index
-            ? ' ' + styles.SuspenseScrubberStepHighlight
-            : '')
-        }
-        onPointerDown={handlePress.bind(null, index)}
-        onMouseEnter={onHoverSegment.bind(null, index)}>
+        label={
+          index === min
+            ? // The first step in the timeline is always a Transition (Initial Paint).
+              // TODO: Support multiple environments.
+              'Initial Paint'
+            : // TODO: Consider adding the name of this specific boundary if this step has only one.
+              'Suspense'
+        }>
         <div
           className={
-            index <= value
-              ? styles.SuspenseScrubberBeadSelected
-              : styles.SuspenseScrubberBead
+            styles.SuspenseScrubberStep +
+            (highlight === index
+              ? ' ' + styles.SuspenseScrubberStepHighlight
+              : '')
           }
-        />
-      </div>,
+          onPointerDown={handlePress.bind(null, index)}
+          onMouseEnter={onHoverSegment.bind(null, index)}>
+          <div
+            className={
+              styles.SuspenseScrubberBead +
+              (index === min
+                ? // The first step in the timeline is always a Transition (Initial Paint).
+                  // TODO: Support multiple environments.
+                  ' ' + styles.SuspenseScrubberBeadTransition
+                : '') +
+              (index <= value ? ' ' + styles.SuspenseScrubberBeadSelected : '')
+            }
+          />
+        </div>
+      </Tooltip>,
     );
   }