packages/dev/style-macro-chrome-plugin/README.md
This is a chrome plugin to assist in debugging the styles applied by the React Spectrum Style Macro.
Until the plugin is published to the Chrome web extension store, the easiest thing to do is to build using the command
yarn workspace style-macro-chrome-plugin build
This will create a dist directory in the directory packages/dev/style-macro-chrome-plugin, you should copy this directory to somewhere permanent on your machine.
Next, open Chrome and go to chrome://extensions/.
Load an unpacked extension, it's a button in the top left, and navigate to the dist directory.
The extension is now registered in Chrome and you can go to storybook or docs, wherever you are working.
Inspect an element on the page to open dev tools and go to the Style Macro panel.
From the root of our monopackage, run
yarn
yarn workspace style-macro-chrome-plugin start
// or build to avoid refresh bugs in HMR
yarn workspace style-macro-chrome-plugin build
This will create a dist directory in the directory packages/dev/style-macro-chrome-plugin which will update anytime the code changes and results in a rebuild.
Now follow the instructions in the above section starting from "Next, open chrome".
If the panel isn't updating with styles, try closing the dev tools and reopening it.
If the extension doesn't appear to have the latest code, try closing the dev tools and reopening it. You may also want to go to the extensions page and either "refresh" or remove and re-add the extension.
If every tab you have open (or many of them) reload when you make local changes to the extension, then go into the extension settings and limit it to localhost or something appropriate.
This extension uses Chrome's standard extension architecture with three main components that communicate via message passing.
--macro-data-{hash}window.__styleMacroDynamic__ (object with map of class name → { style, loc } and an internal interval timer for cleanup)content-script.js)window.postMessage({ action: 'stylemacro-class-changed' }) from Mutation Observer on page to background scriptwindow.postMessage({ action: 'stylemacro-class-changed', elementId }) from pagechrome.runtime.sendMessage({ action: 'stylemacro-class-changed', elementId }) to backgroundbackground.js)chrome.runtime.onConnect({ name: 'devtools-page' }) from DevToolsport.onMessage({ type: 'stylemacro-init' }) from DevToolschrome.runtime.onMessage({ action: 'stylemacro-class-changed', elementId }) from content scriptport.postMessage({ action: 'stylemacro-class-changed', elementId }) to DevToolsdevtool.js)-macro-static-{hash} → reads --macro-data-{hash} custom property via getComputedStyle() on the element-macro-dynamic-{hash} → reads from window.__styleMacroDynamic__.map["-macro-dynamic-{hash}"] (plain JSON)chrome.devtools.panels.elements.onSelectionChangedclass attribute for changesport.onMessage({ action: 'stylemacro-class-changed', elementId }) from background (triggers refresh)chrome.runtime.connect({ name: 'devtools-page' }) to establish connectionport.postMessage({ type: 'stylemacro-init', tabId }) to backgroundStatic macros are generated when style macro conditions don't change at runtime. The macro data is embedded directly into the CSS as a uniquely-named custom property.
┌─────────────────┐
│ DevTools Panel │ User selects element with -macro-static-{hash} class
└────────┬────────┘
│ Extract hash from className
│ Read --macro-data-{hash} via getComputedStyle($0)
↓
┌─────────────────┐
│ Page DOM/CSS │ Returns custom property value for specific hash
└────────┬────────┘
│ getPropertyValue('--macro-data-{hash}')
│ { loc: "...", style: {...} }
↓
┌─────────────────┐
│ DevTools Panel │ Parses and displays in sidebar
└─────────────────┘
Key Design: Each static macro has its own uniquely-named custom property (--macro-data-{hash}), which avoids CSS cascade issues when reading multiple macro data from the same element.
Dynamic macros are generated when style macro conditions can change at runtime. Data is written to a global JavaScript variable; a timer on the same object cleans up entries whose class is no longer present in the DOM.
┌─────────────────┐
│ Page Context │
│ (style-macro) │ Runtime evaluation with dynamic conditions
└────────┬────────┘
│ 1. Ensure window.__styleMacroDynamic__ exists ({ map: {}, _timer })
│ 2. Set map["-macro-dynamic-{hash}"] = { style, loc } (plain JSON)
│ 3. Timer (e.g. every 5 min) removes entries with no matching element in DOM
↓
┌─────────────────┐
│ Page (global) │ window.__styleMacroDynamic__.map["-macro-dynamic-{hash}"] = { style, loc }
└─────────────────┘
When the user selects an element or the panel refreshes, DevTools reads macro data as follows.
┌─────────────────┐
│ DevTools Panel │ User selects element with -macro-static-{hash} or -macro-dynamic-{hash} class
└────────┬────────┘
│ Extract hash from className
↓
┌─────────────────┐
│ DevTools Panel │ Static: getComputedStyle($0).getPropertyValue('--macro-data-{hash}') → JSON.parse
│ │ Dynamic: window.__styleMacroDynamic__.map["-macro-dynamic-{hash}"] → already { style, loc }
└────────┬────────┘
│
↓
┌─────────────────┐
│ DevTools Panel │ Parses and displays in sidebar
└─────────────────┘
Note: Static macros are read from CSS custom properties on the inspected element. Dynamic macros are read from the page’s global window.__styleMacroDynamic__.map (no CSS involved).
When you select an element, the DevTools panel automatically watches for className changes and refreshes the panel.
Chrome extensions prevent direct communication between DevTools and content scripts for security reasons. The background script acts as a trusted intermediary.
The style macro generates different class name patterns based on whether the styles can change at runtime:
Static Macros (-macro-static-{hash}):
style({ color: 'red' }))--macro-data-{hash}: '{...JSON...}'getComputedStyle($0).getPropertyValue('--macro-data-{hash}')Dynamic Macros (-macro-dynamic-{hash}):
style({color: {default: 'blue', isActive: 'red'}}))window.__styleMacroDynamic__.map["-macro-dynamic-{hash}"] as plain JSON { style, loc }window.__styleMacroDynamic__ periodically removes map entries whose class is no longer used in the DOMwindow.__styleMacroDynamic__.map["-macro-dynamic-{hash}"]chrome.runtime.connect() with port-based messagingchrome.runtime.sendMessage() callstabId → DevTools port for routing messagesStatic Macros (in main CSS):
.-macro-static-zsZ9Dc {
--macro-data-zsZ9Dc: '{"style":{"paddingX":"4"},"loc":"packages/@react-spectrum/s2/src/Button.tsx:67"}';
}
Dynamic Macros (in page global):
window.__styleMacroDynamic__ = {
map: {
"-macro-dynamic-zsZ9Dc": { style: { paddingX: "4" }, loc: "packages/@react-spectrum/s2/src/Button.tsx:67" },
"-macro-dynamic-abc123": { style: {...}, loc: "..." }
},
_timer: 123 // setInterval for cleanup of unused entries
};
| Message Type | Direction | Purpose |
|---|---|---|
stylemacro-init | DevTools → Background | Establish connection with tabId |
stylemacro-class-changed | Page → Content → Background → DevTools | Notify that selected element's className changed, triggering panel refresh |
Enable debug logs by uncommenting the console.log() lines in each component:
devtool.js → debugLog() functioncontent-script.js → debugLog() functionView logs in:
[Content Script] and [DevTools] prefixes)chrome://extensions → click "service worker")