Skip to content

[DevTools] Refactor imperative theme code #21900

@bvaughn

Description

@bvaughn

DevTools defaults a "light" and a "dark" theme in a root-level CSS file:

:root {
/**
* IMPORTANT: When new theme variables are added below– also add them to SettingsContext updateThemeVariables()
*/
/* Light theme */
--light-color-attribute-name: #ef6632;
--light-color-attribute-name-not-editable: #23272f;
--light-color-attribute-name-inverted: rgba(255, 255, 255, 0.7);
--light-color-attribute-value: #1a1aa6;
--light-color-attribute-value-inverted: #ffffff;
--light-color-attribute-editable-value: #1a1aa6;
--light-color-background: #ffffff;
--light-color-background-hover: rgba(0, 136, 250, 0.1);
--light-color-background-inactive: #e5e5e5;
--light-color-background-invalid: #fff0f0;
--light-color-background-selected: #0088fa;
--light-color-button-background: #ffffff;
--light-color-button-background-focus: #ededed;
--light-color-button: #5f6673;
--light-color-button-disabled: #cfd1d5;
--light-color-button-active: #0088fa;
--light-color-button-focus: #23272f;
--light-color-button-hover: #23272f;
--light-color-border: #eeeeee;
--light-color-commit-did-not-render-fill: #cfd1d5;
--light-color-commit-did-not-render-fill-text: #000000;
--light-color-commit-did-not-render-pattern: #cfd1d5;
--light-color-commit-did-not-render-pattern-text: #333333;
--light-color-commit-gradient-0: #37afa9;
--light-color-commit-gradient-1: #63b19e;
--light-color-commit-gradient-2: #80b393;
--light-color-commit-gradient-3: #97b488;
--light-color-commit-gradient-4: #abb67d;
--light-color-commit-gradient-5: #beb771;
--light-color-commit-gradient-6: #cfb965;
--light-color-commit-gradient-7: #dfba57;
--light-color-commit-gradient-8: #efbb49;
--light-color-commit-gradient-9: #febc38;
--light-color-commit-gradient-text: #000000;
--light-color-component-name: #6a51b2;
--light-color-component-name-inverted: #ffffff;
--light-color-component-badge-background: rgba(0, 0, 0, 0.1);
--light-color-component-badge-background-inverted: rgba(255, 255, 255, 0.25);
--light-color-component-badge-count: #777d88;
--light-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7);
--light-color-console-error-badge-text: #ffffff;
--light-color-console-error-background: #fff0f0;
--light-color-console-error-border: #ffd6d6;
--light-color-console-error-icon: #eb3941;
--light-color-console-error-text: #fe2e31;
--light-color-console-warning-badge-text: #000000;
--light-color-console-warning-background: #fffbe5;
--light-color-console-warning-border: #fff5c1;
--light-color-console-warning-icon: #f4bd00;
--light-color-console-warning-text: #64460c;
--light-color-context-background: rgba(0,0,0,.9);
--light-color-context-background-hover: rgba(255, 255, 255, 0.1);
--light-color-context-background-selected: #178fb9;
--light-color-context-border: #3d424a;
--light-color-context-text: #ffffff;
--light-color-context-text-selected: #ffffff;
--light-color-dim: #777d88;
--light-color-dimmer: #cfd1d5;
--light-color-dimmest: #eff0f1;
--light-color-error-background: hsl(0, 100%, 97%);
--light-color-error-border: hsl(0, 100%, 92%);
--light-color-error-text: #ff0000;
--light-color-expand-collapse-toggle: #777d88;
--light-color-link: #0000ff;
--light-color-modal-background: rgba(255, 255, 255, 0.75);
--light-color-bridge-version-npm-background: #eff0f1;
--light-color-bridge-version-npm-text: #000000;
--light-color-bridge-version-number: #0088fa;
--light-color-primitive-hook-badge-background: #e5e5e5;
--light-color-primitive-hook-badge-text: #5f6673;
--light-color-record-active: #fc3a4b;
--light-color-record-hover: #3578e5;
--light-color-record-inactive: #0088fa;
--light-color-scroll-thumb: #c2c2c2;
--light-color-scroll-track: #fafafa;
--light-color-search-match: yellow;
--light-color-search-match-current: #f7923b;
--light-color-selected-tree-highlight-active: rgba(0, 136, 250, 0.1);
--light-color-selected-tree-highlight-inactive: rgba(0, 0, 0, 0.05);
--light-color-shadow: rgba(0, 0, 0, 0.25);
--light-color-tab-selected-border: #0088fa;
--light-color-text: #000000;
--light-color-text-invalid: #ff0000;
--light-color-text-selected: #ffffff;
--light-color-toggle-background-invalid: #fc3a4b;
--light-color-toggle-background-on: #0088fa;
--light-color-toggle-background-off: #cfd1d5;
--light-color-toggle-text: #ffffff;
--light-color-tooltip-background: rgba(0, 0, 0, 0.9);
--light-color-tooltip-text: #ffffff;
/* Dark theme */
--dark-color-attribute-name: #9d87d2;
--dark-color-attribute-name-not-editable: #ededed;
--dark-color-attribute-name-inverted: #282828;
--dark-color-attribute-value: #cedae0;
--dark-color-attribute-value-inverted: #ffffff;
--dark-color-attribute-editable-value: yellow;
--dark-color-background: #282c34;
--dark-color-background-hover: rgba(255, 255, 255, 0.1);
--dark-color-background-inactive: #3d424a;
--dark-color-background-invalid: #5c0000;
--dark-color-background-selected: #178fb9;
--dark-color-button-background: #282c34;
--dark-color-button-background-focus: #3d424a;
--dark-color-button: #afb3b9;
--dark-color-button-active: #61dafb;
--dark-color-button-disabled: #4f5766;
--dark-color-button-focus: #a2e9fc;
--dark-color-button-hover: #ededed;
--dark-color-border: #3d424a;
--dark-color-commit-did-not-render-fill: #777d88;
--dark-color-commit-did-not-render-fill-text: #000000;
--dark-color-commit-did-not-render-pattern: #666c77;
--dark-color-commit-did-not-render-pattern-text: #ffffff;
--dark-color-commit-gradient-0: #37afa9;
--dark-color-commit-gradient-1: #63b19e;
--dark-color-commit-gradient-2: #80b393;
--dark-color-commit-gradient-3: #97b488;
--dark-color-commit-gradient-4: #abb67d;
--dark-color-commit-gradient-5: #beb771;
--dark-color-commit-gradient-6: #cfb965;
--dark-color-commit-gradient-7: #dfba57;
--dark-color-commit-gradient-8: #efbb49;
--dark-color-commit-gradient-9: #febc38;
--dark-color-commit-gradient-text: #000000;
--dark-color-component-name: #61dafb;
--dark-color-component-name-inverted: #282828;
--dark-color-component-badge-background: rgba(255, 255, 255, 0.25);
--dark-color-component-badge-background-inverted: rgba(0, 0, 0, 0.25);
--dark-color-component-badge-count: #8f949d;
--dark-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7);
--dark-color-console-error-badge-text: #000000;
--dark-color-console-error-background: #290000;
--dark-color-console-error-border: #5c0000;
--dark-color-console-error-icon: #eb3941;
--dark-color-console-error-text: #fc7f7f;
--dark-color-console-warning-badge-text: #000000;
--dark-color-console-warning-background: #332b00;
--dark-color-console-warning-border: #665500;
--dark-color-console-warning-icon: #f4bd00;
--dark-color-console-warning-text: #f5f2ed;
--dark-color-context-background: rgba(255,255,255,.9);
--dark-color-context-background-hover: rgba(0, 136, 250, 0.1);
--dark-color-context-background-selected: #0088fa;
--dark-color-context-border: #eeeeee;
--dark-color-context-text: #000000;
--dark-color-context-text-selected: #ffffff;
--dark-color-dim: #8f949d;
--dark-color-dimmer: #777d88;
--dark-color-dimmest: #4f5766;
--dark-color-error-background: #200;
--dark-color-error-border: #900;
--dark-color-error-text: #f55;
--dark-color-expand-collapse-toggle: #8f949d;
--dark-color-link: #61dafb;
--dark-color-modal-background: rgba(0, 0, 0, 0.75);
--dark-color-bridge-version-npm-background: rgba(0, 0, 0, 0.25);
--dark-color-bridge-version-npm-text: #ffffff;
--dark-color-bridge-version-number: yellow;
--dark-color-primitive-hook-badge-background: rgba(0, 0, 0, 0.25);
--dark-color-primitive-hook-badge-text: rgba(255, 255, 255, 0.7);
--dark-color-record-active: #fc3a4b;
--dark-color-record-hover: #a2e9fc;
--dark-color-record-inactive: #61dafb;
--dark-color-scroll-thumb: #afb3b9;
--dark-color-scroll-track: #313640;
--dark-color-search-match: yellow;
--dark-color-search-match-current: #f7923b;
--dark-color-selected-tree-highlight-active: rgba(23, 143, 185, 0.15);
--dark-color-selected-tree-highlight-inactive: rgba(255, 255, 255, 0.05);
--dark-color-shadow: rgba(0, 0, 0, 0.5);
--dark-color-tab-selected-border: #178fb9;
--dark-color-text: #ffffff;
--dark-color-text-invalid: #ff8080;
--dark-color-text-selected: #ffffff;
--dark-color-toggle-background-invalid: #fc3a4b;
--dark-color-toggle-background-on: #178fb9;
--dark-color-toggle-background-off: #777d88;
--dark-color-toggle-text: #ffffff;
--dark-color-tooltip-background: rgba(255, 255, 255, 0.9);
--dark-color-tooltip-text: #000000;

The current theme value ("light", "dark", or "auto") is stored in the ThemeContext and persisted between sessions using the localStorage API:

const [theme, setTheme] = useLocalStorage<Theme>(
'React::DevTools::theme',
'auto',
);

When the theme changes, a layout effect updates root-level CSS variables:

useLayoutEffect(() => {
switch (theme) {
case 'light':
updateThemeVariables('light', documentElements);
break;
case 'dark':
updateThemeVariables('dark', documentElements);
break;
case 'auto':
updateThemeVariables(browserTheme, documentElements);
break;
default:
throw Error(`Unsupported theme value "${theme}"`);
}
}, [browserTheme, theme, documentElements]);

This has an unfortunate side effect of creating a race condition between any other imperative code (e.g. a Canvas rendering component) that may want to read the current CSS variables. This code cannot run before the code above or it will read previous/stale values.

We should refactor this code to declaratively share theme variables to its subtree, e.g.:

function ThemeProvider({ value, children }) {
  // ...

  return (
    <ThemeContext.Provider value={theme}>
      <div
        style={{
          '--color-attribute-name': '#ef6632',
          '--color-attribute-name-not-editable': '#23272f',
          // ...
        }}
      >
        {children}
      </div>
    </ThemeContext.Provider>
  );
}

We will also need to update any place that reads these computed styles, e.g.

// Sizes and paddings/margins are all rem-based,
// so update the root font-size as well when the display preference changes.
const computedStyle = getComputedStyle((document.body: any));
const fontSize = computedStyle.getPropertyValue(
`--${displayDensity}-root-font-size`,
);
const root = ((document.querySelector(':root'): any): HTMLElement);
root.style.fontSize = fontSize;

A similar approach is used for font size ("comfortable" or "compact"). We should do the same refactor for this.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions