Skip to content

[DevTools] Refactor imperative theme code #21900

Closed
@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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions