Patch history and review: Phabricator D274481
Intro
In his presentation at FOSDEM 22 - Firefox Desktop Development 101 - Mike Conley explains (at the 22:20 timestamp) that the platform used to render the contents of the webpages in the browser is the same platform that renders its UI. This means that the styling that you see on the chrome (browser UI) of your Firefox is done with CSS, the logic that performs things like switching tabs is mostly written in JavaScript, and so forth. As a matter of fact, if you have the Mozilla Firefox browser installed, you can type chrome://browser/content/browser.xhtml in the address bar right now and see Firefox’s chrome occupying the content display area!
This bug was circumscribed to only one of the DevTools panels. All panels are located within devtools/client/ in the Firefox codebase, each has its own subdirectory. For this patch, we’re working within devtools/client/debugger/ - the JavaScript Debugger panel. The file devtools/client/debugger/src/debugger.css serves as the main stylesheet entry point - it doesn’t contain styles itself, but rather @import statements that pull in CSS from each component. When the Debugger panel loads, this single file orchestrates loading all the stylesheets needed to render the complete UI.
Inside devtools/client/debugger/src/components/ you’ll find React components that compose the Debugger’s UI. These are organized into subdirectories reflecting the panel’s layout:
Editor/- The source code viewer, tabs, and breakpoint guttersPrimaryPanes/- The left sidebar containing the source tree and project searchSecondaryPanes/- The right sidebar with call stack, scopes, and watch expressionsshared/- Reusable components used across the Debugger
The shared/ directory is particularly relevant to this patch - it contained AccessibleImage, a component that renders all the icons you see throughout the Debugger panel (folder icons in the source tree, play/pause buttons in the command bar, etc.). Though organized into separate directories, all DevTools panels ultimately load their CSS into a single global scope, making class name collisions between panels an ever-present risk.
The Problem
As the discussion in this GitHub issue shows, the Debugger’s CSS class names are too generic. Names like .img, .frame, .title, and .worker existing in a global namespace shared by all DevTools panels is, as Florens put it, “the rough equivalent of writing JS like” this:
window.img = function() { ... }
window.frame = function() { ... }
window.title = function() { ... }
window.worker = function() { ... }
This bug specifically is about a React component called AccessibleImage that was defined like this:
const AccessibleImage = props => {
return React.createElement("span", {
...props,
className: classnames("img", props.className),
});
};
The classnames is a utility function that combines CSS class names into a single string. So when you wrote <AccessibleImage className="close" /> React rendered <span class="img close"></span>. The component always added img as a base class, then appended whatever you passed via className. The corresponding CSS would use these classes as selectors:
.img {
width: 16px;
height: 16px;
background-color: var(--theme-icon-color);
}
.img.close {
mask-image: url(debugger/images/close.svg);
}
Which was prone to cause cross-panel namespacing conflicts.
The Approach
For this patch I applied the BEM (‘bem’ means ‘well’ in my native tongue) namespacing strategy to the icon generator component used throughout the Debugger panel. The core principle is simple: prefix everything with a namespace unique to the component or module. In this case, dbg- signals “Debugger panel”. This makes collisions with other panels (Console, Network Monitor, Inspector) virtually impossible, even with all panels sharing the same global CSS namespace.
The implementation breaks down into four major efforts:
- Rename the
AccessibleImagecomponent toDebuggerImageand update its API to enforce namespaced class generation. - Change the base class from the generic
imgto the namespaceddbg-img. - Systematically rename all image-specific classes from
<name>todbg-img-<name>. - Update any references to the old class names.
Implementation
1. Modifying the component
This is the core of the work, the change which everything else emanates from. The renaming of the component itself (from AccessibleImage to DebuggerImage) was deemed wise since the very nature of its new API made its Debugger panel exclusive usage clear. Here’s how the new component is defined (in the DebuggerImage.js file):
const DebuggerImage = props => {
const { name, className, ...attributes } = props;
return React.createElement("span", {
...attributes,
className: classnames("dbg-img", `dbg-img-${name}`, className),
});
};
Its prop API now separates two concerns that were previously conflated: icon identity and additional styling. The new required name prop specifies which icon to render (arrow, close, worker), while the optional className prop handles any additional state or modifier classes (expanded, spin). This separation makes the component’s purpose explicit and prevents misuse.
All call sites changed from:
// Old
<AccessibleImage className="close" />
<AccessibleImage className="loader spin" />
// New
<DebuggerImage name="close" />
<DebuggerImage name="loader" className="spin" />
Files affected: App.js, Footer.js, Tabs.js, ProjectSearch.js, SourcesTree.js, SourcesTreeItem.js, CommandBar.js, Expressions.js, Frame.js, Group.js, Scopes.js, Thread.js, WhyPaused.js, index.js, CloseButton.js, CommandBarButton.js, PaneToggleButton.js, EventListeners.js, ResultList.js, SearchInput.js, SourceIcon.js.
Build files affected: moz.build, jar.mn, debugger.css (import path).
2. Changing the base style
The component’s base class change (line 14 of DebuggerImage.js):
className: classnames("dbg-img", ...)
Required renaming and updating AccessibleImage.css to DebuggerImage.css. The base styles now use the namespaced selector:
.dbg-img {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: middle;
background-color: var(--theme-icon-color);
mask-size: contain;
/* ... other base icon styles */
}
This entailed editing pretty much the entire DebuggerImage.css, and SourceIcon.css files.
3. Editing CSS selectors
Now that the component defined new class names (line 14 of DebuggerImage.js):
className: classnames("dbg-img", `dbg-img-${name}`, className)
All class names changed from <name> to dbg-img-<name>. So in the CSS files, all icon-specific selectors had to change accordingly:
/* Old */
.img.arrow { ... }
.img.close { ... }
.img.search { ... }
/* New */
dbg-img-arrow { ... }
dbg-img-close { ... }
dbg-img-search { ... }
This resulted in the refactoring of the files: SearchInput.css, Dropdown.css, Breakpoints.css, Footer.css, Tabs.css, ProjectSearch.css, Sources.css, Frames.css, Group.css, Scopes.css, Threads.css, Accordion.css, CloseButton.css, CommandBarButton.css, PaneToggleButton.css, Popup.css.
4. Updating test references
Tests that used querySelector with the old class names also needed to be changed, and being able to run the tests myself to see where they failed was a very effective way to detect that. For this patch, an artifact build of Firefox is enough. Because we are not editing C++, we can rely on the downloaded, pre-compiled C++ that comes in this build.
Running the mochitest suite revealed which test files needed attention. In the end these were the files that had to be changed: shared-head.js, browser_dbg-blackbox-all.js, browser_dbg-blackbox-original.js, browser_dbg-quick-open.js, browser_dbg-react-app.js, browser_dbg-tabs.js, browser_dbg-features-source-tree.js.
Adjusting CSS Specificity
After submitting the patch, the maintainer brought to my attention that some of my CSS editing altered the specificity of the selections. He ran CI algorithms and found this issue. This turned out to be a great opportunity to get acquainted with this CSS quirk.
When multiple CSS rules target the same element, the browser needs a way to decide which one wins. It then uses specificity algorithms that calculate the selection priority by giving weights for the selectors. These weights can be represented by 3 columns of decreasing value: ID - CLASS - TYPE.
This table exemplifies its calculation:
| Selector | ID | CLASS | TYPE | Specificity |
|---|---|---|---|---|
p | 0 | 0 | 1 | 0-0-1 |
.arrow | 0 | 1 | 0 | 0-1-0 |
.img.arrow | 0 | 2 | 0 | 0-2-0 |
#sidebar .img | 1 | 1 | 0 | 1-1-0 |
Each ID adds 1 to the ID column. Each class selector adds 1 to the CLASS column. TYPE selectors (element names like div, span) add to the TYPE column. Columns are compared left-to-right. A higher value in an earlier column wins outright, regardless of later columns.
Now tying it with the patch submitted, the original Debugger CSS sometimes used compound selectors:
.img.arrow { ... }
And my patch would change it to:
.dbg-img-arrow { ... }
Which drops the specificity from 0-2-0 to 0-1-0. This loss of specificity, in a large and complex codebases such as Firefox, is very dangerous, as other selectors might unexpectedly win, depending on source order. Styles break in subtle, hard-to-debug ways.
To avoid this drop in specificity, the maintainer proposed the following pattern:
/* Old: 0-2-0 */
.img.arrow { ... }
/* New: 0-2-0 (preserved) */
.dbg-img.dbg-img-arrow { ... }
This pattern was applied to the following CSS files: DebuggerImage.css, SourceIcon.css, SearchInput.css, Dropdown.css, ProjectSearch.css, Tabs.css, Sources.css, Frames.css, Scopes.css, Breakpoints.css, Footer.css, Group.css, Threads.css, Accordion.css, CloseButton.css, CommandBarButton.css, PaneToggleButton.css, Popup.css.
Takeaways
Following the discussion in the GitHub issue page, this patch is part of the systematic effort to eliminate CSS collisions across Firefox DevTools. That issue laid out the architectural problem, and this patch demonstrates the solution pattern: manual BEM namespacing applied methodically to one of the panel’s most widely-used components. The DebuggerImage refactor serves as a template for how other Debugger components can be namespaced, moving the panel incrementally toward collision-proof CSS, without requiring build tool changes or framework migrations.
This bug sat open for six years. Multiple contributors have attempted fixes, but I was the one to push it through: refactored every call site, adjusted CSS specificity to preserve existing behavior, and validated the changes across the test suite. There’s something uniquely satisfying about closing issues that have lingered on the backlog for years.