Repository: facebook/react
Pull request: [DevTools] Tweak the rects design and create multi-environment color scheme

<img width="1011" height="811" alt="Screenshot 2025-10-16 at 2 20 46 PM" src="https://github.com/user-attachments/assets/6dea3962-d369-4823-b44f-2c62b566c8f1" />

The selection is now clearer with a wider outline which spans the bounding box if there are multi rects.

The color now gets darked changes on hover with a slight animation.

The colors are now mixed from constants defined which are consistently used in the rects, the time span in the "suspended by" side bar and the scrubber. I also have constants defined for "server" and "other" debug environments which will be used in a follow up.

Changed files:
filename: packages/react-devtools-shared/src/devtools/constants.js
patch: @@ -136,8 +136,6 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
     '--color-timeline-text-dim-color': '#ccc',
     '--color-timeline-react-work-border': '#eeeeee',
     '--color-timebar-background': '#f6f6f6',
-    '--color-timespan-background': '#62bc6a',
-    '--color-timespan-background-errored': '#d57066',
     '--color-search-match': 'yellow',
     '--color-search-match-current': '#f7923b',
     '--color-selected-tree-highlight-active': 'rgba(0, 136, 250, 0.1)',
@@ -156,6 +154,14 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
     '--color-warning-text-color': '#ffffff',
     '--color-warning-text-color-inverted': '#fd4d69',
 
+    '--color-suspense': '#0088fa',
+    '--color-transition': '#6a51b2',
+    '--color-suspense-server': '#62bc6a',
+    '--color-transition-server': '#3f7844',
+    '--color-suspense-other': '#f3ce49',
+    '--color-transition-other': '#917b2c',
+    '--color-suspense-errored': '#d57066',
+
     // The styles below should be kept in sync with 'root.css'
     // They are repeated there because they're used by e.g. tooltips or context menus
     // which get rendered outside of the DOM subtree (where normal theme/styles are written).
@@ -290,8 +296,6 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
     '--color-timeline-text-dim-color': '#555b66',
     '--color-timeline-react-work-border': '#3d424a',
     '--color-timebar-background': '#1d2129',
-    '--color-timespan-background': '#62bc6a',
-    '--color-timespan-background-errored': '#d57066',
     '--color-search-match': 'yellow',
     '--color-search-match-current': '#f7923b',
     '--color-selected-tree-highlight-active': 'rgba(23, 143, 185, 0.15)',
@@ -311,6 +315,14 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
     '--color-warning-text-color': '#ffffff',
     '--color-warning-text-color-inverted': '#ee1638',
 
+    '--color-suspense': '#61dafb',
+    '--color-transition': '#6a51b2',
+    '--color-suspense-server': '#62bc6a',
+    '--color-transition-server': '#3f7844',
+    '--color-suspense-other': '#f3ce49',
+    '--color-transition-other': '#917b2c',
+    '--color-suspense-errored': '#d57066',
+
     // The styles below should be kept in sync with 'root.css'
     // They are repeated there because they're used by e.g. tooltips or context menus
     // which get rendered outside of the DOM subtree (where normal theme/styles are written).

filename: packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css
patch: @@ -128,13 +128,13 @@
 .TimeBarSpan, .TimeBarSpanErrored {
   position: absolute;
   border-radius: 0.125rem;
-  background-color: var(--color-timespan-background);
+  background-color: var(--color-suspense);
   width: 100%;
   height: 100%;
 }
 
 .TimeBarSpanErrored {
-  background-color: var(--color-timespan-background-errored);
+  background-color: var(--color-suspense-errored);
 }
 
 .SmallHeader {

filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css
patch: @@ -1,12 +1,20 @@
 .SuspenseRectsContainer {
   padding: .25rem;
   cursor: pointer;
-  outline: 1px solid var(--color-component-name);
+  outline-color: var(--color-transition);
+  outline-style: solid;
+  outline-width: 1px;
   border-radius: 0.25rem;
+  background-color: color-mix(in srgb, var(--color-transition) 5%, transparent);
+}
+
+.SuspenseRectsContainer:hover:not(:has(.SuspenseRectsBoundary:hover))[data-highlighted='false'] {
+  outline-width: 1px;
 }
 
 .SuspenseRectsContainer[data-highlighted='true'] {
-  background: var(--color-dimmest);
+  outline-style: solid;
+  outline-width: 4px;
 }
 
 .SuspenseRectsViewBox {
@@ -15,6 +23,11 @@
 
 .SuspenseRectsBoundary {
   pointer-events: all;
+  border-radius: 0.125rem;
+}
+
+.SuspenseRectsBoundary[data-visible='false'] {
+  background-color: transparent;
 }
 
 .SuspenseRectsBoundaryChildren {
@@ -28,31 +41,47 @@
 .SuspenseRectsRect {
   box-shadow: var(--elevation-4);
   pointer-events: all;
+  cursor: pointer;
+  border-radius: 0.125rem;
+  background-color: color-mix(in srgb, var(--color-background) 50%, var(--color-suspense) 25%);
+  backdrop-filter: grayscale(100%);
+  transition: background-color 0.2s ease-in;
+  outline-color: var(--color-suspense);
   outline-style: solid;
   outline-width: 1px;
-  border-radius: 0.125rem;
-  cursor: pointer;
 }
 
 .SuspenseRectsScaledRect {
   position: absolute;
-  outline-color: var(--color-background-selected);
 }
 
 .SuspenseRectsScaledRect[data-visible='false'] {
   pointer-events: none;
   outline-width: 0;
 }
 
-.SuspenseRectsScaledRect[data-suspended='true'] {
-  opacity: 0.3;
+.SuspenseRectsBoundary[data-suspended='true'] {
+  opacity: 0.33;
 }
 
 /* highlight this boundary */
-.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
-  background-color: var(--color-background-hover);
+.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect {
+  background-color: color-mix(in srgb, var(--color-background) 50%, var(--color-suspense) 50%);
+  transition: background-color 0.2s ease-out;
+}
+
+.SuspenseRectsBoundary[data-selected='true'] {
+  box-shadow: var(--elevation-4);
+}
+
+.SuspenseRectOutline {
+  outline-color: var(--color-suspense);
+  outline-style: solid;
+  outline-width: 4px;
+  border-radius: 0.125rem;
+  pointer-events: none;
 }
 
-.SuspenseRectsRect[data-highlighted='true'] {
-  background-color: var(--color-selected-tree-highlight-active);
+.SuspenseRectsBoundary[data-selected='true'] > .SuspenseRectsRect {
+   box-shadow: none;
 }

filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js
patch: @@ -36,13 +36,15 @@ function ScaledRect({
   rect,
   visible,
   suspended,
+  selected,
   adjust,
   ...props
 }: {
   className: string,
   rect: Rect,
   visible: boolean,
   suspended: boolean,
+  selected?: boolean,
   adjust?: boolean,
   ...
 }): React$Node {
@@ -58,6 +60,7 @@ function ScaledRect({
       className={styles.SuspenseRectsScaledRect + ' ' + className}
       data-visible={visible}
       data-suspended={suspended}
+      data-selected={selected}
       style={{
         // Shrink one pixel so that the bottom outline will line up with the top outline of the next one.
         width: adjust ? 'calc(' + width + ' - 1px)' : width,
@@ -152,6 +155,7 @@ function SuspenseRects({
       rect={boundingBox}
       className={styles.SuspenseRectsBoundary}
       visible={visible}
+      selected={selected}
       suspended={suspense.isSuspended}>
       <ViewBox.Provider value={boundingBox}>
         {visible &&
@@ -162,7 +166,6 @@ function SuspenseRects({
                 key={index}
                 className={styles.SuspenseRectsRect}
                 rect={rect}
-                data-highlighted={selected}
                 adjust={true}
                 onClick={handleClick}
                 onDoubleClick={handleDoubleClick}
@@ -182,6 +185,13 @@ function SuspenseRects({
             })}
           </ScaledRect>
         )}
+        {selected ? (
+          <ScaledRect
+            className={styles.SuspenseRectOutline}
+            rect={boundingBox}
+            adjust={true}
+          />
+        ) : null}
       </ViewBox.Provider>
     </ScaledRect>
   );

filename: packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css
patch: @@ -40,22 +40,22 @@
 .SuspenseScrubberBead {
   flex: 1;
   height: 0.5rem;
-  background: var(--color-background-selected);
   border-radius: 0.5rem;
-  background: var(--color-selected-tree-highlight-active);
-  transition: all 0.3s ease-in-out;
+  background: color-mix(in srgb, var(--color-suspense) 10%, transparent);
+  transition: all 0.3s ease-in;
 }
 
 .SuspenseScrubberBeadSelected {
   height: 1rem;
-  background: var(--color-background-selected);
+  background: var(--color-suspense);
 }
 
 .SuspenseScrubberBeadTransition {
-  background: var(--color-component-name);
+  background: var(--color-transition);
 }
 
 .SuspenseScrubberStepHighlight > .SuspenseScrubberBead,
 .SuspenseScrubberStep:hover > .SuspenseScrubberBead {
   height: 0.75rem;
+  transition: all 0.3s ease-out;
 }