firefox-theming
Firefox Theming
Source of truth
Firefox ships CSS inside omni.ja (a zip file). Always extract and read source files before writing or auditing userChrome rules.
Extract source
OMNI=$(ls /nix/store/*firefox-unwrapped*/lib/firefox/browser/omni.ja 2>/dev/null | head -1)
OMNI_TK=$(ls /nix/store/*firefox-unwrapped*/lib/firefox/omni.ja 2>/dev/null | head -1)
DEST=/tmp/ff-omni
mkdir -p "$DEST" && cd "$DEST"
# CSS
unzip -o "$OMNI" "chrome/browser/skin/classic/browser/*.css" "chrome/browser/skin/classic/browser/**/*.css" 2>/dev/null
unzip -o "$OMNI_TK" "chrome/toolkit/skin/classic/global/*.css" "chrome/toolkit/skin/classic/global/**/*.css" 2>/dev/null
# DevTools
unzip -o "$OMNI" "chrome/devtools/skin/*.css" 2>/dev/null
# XUL/HTML
unzip -o "$OMNI" "chrome/browser/content/browser/browser.xhtml" 2>/dev/null
Verify element existence: grep 'id="element-id"' $DEST/chrome/browser/content/browser/browser.xhtml.
Reference
references/source-map.md — complete variable/element/class reference for:
- Browser CSS (
chrome/browser/skin/classic/browser/): browser-colors, browser-shared, toolbarbuttons, urlbar-searchbar, urlbarView, tabs, sidebar, contextmenu, identity-block, panelUI-shared, unified-extensions, downloads, findbar, and more - Toolkit CSS (
chrome/toolkit/skin/classic/global/): global-shared, popup, menu, findbar, in-content/common-shared, design-system/tokens-shared - DevTools CSS (
chrome/devtools/skin/): variables.css theme variables - browser.xhtml: structural IDs, urlbar IDs, sidebar IDs, toolbar button IDs, key classes, non-existent IDs
Consult the source-map first. Extract and read the actual source file when the map lacks detail.
Workflow
- Consult source-map.md for the section being edited
- Extract source CSS from omni.ja when needed
- Read the relevant source file
- Verify every selector targets elements/classes/IDs that exist in source CSS or browser.xhtml
- Verify every CSS variable override matches a variable defined in source
- Use variables over direct property overrides when the source uses variables
- Remove rules that match nothing
File placement
| Target | File | Selector scope |
|---|---|---|
| Browser chrome | userChrome.css |
:root, #navigator-toolbox, etc. |
| Web content pages | userContent.css |
@-moz-document rules |
| DevTools | userContent.css |
:root.theme-dark / :root.theme-light |
DevTools theming
Override DevTools variables in userContent.css using :root.theme-dark (or :root.theme-light).
- Do NOT use
@-moz-document url-prefix("chrome://devtools/content/")— doesn't match DevTools documents - Do NOT put DevTools overrides in userChrome.css — DevTools runs in a separate iframe
- Variables are in
chrome/devtools/skin/variables.css - Reference: one-monokai-firefox-devtools
Search mode indicators
Two systems exist. Hide both when removing the chip:
- Legacy:
#urlbar-search-mode-indicator— shown via#urlbar[searchmode] > .urlbar-input-container > #urlbar-search-mode-indicator - Scotch Bonnet:
#searchmode-switcher-chicklet— shown via#urlbar[searchmode] #searchmode-switcher-chicklet(gated behindbrowser.urlbar.scotchBonnet.enableOverrideorbrowser.urlbar.searchModeSwitcher.featureGate) - Related:
#urlbar-searchmode-switcher,#searchmode-switcher-title,#searchmode-switcher-close,#searchmode-switcher-dropmarker
When CSS doesn't work
Search online for working reference implementations before guessing. One working example beats ten theories. The MrOtherGuy/firefox-csshacks repo is the canonical reference for userChrome hacks.
Tabs below content
Use CSS grid on #main-window > body, NOT order / -moz-box-ordinal-group:
@media not -moz-pref("sidebar.verticalTabs") {
#main-window > body {
display: grid !important;
grid-template-rows: repeat(8, max-content) 1fr;
grid-auto-rows: auto;
}
#navigator-toolbox {
display: contents;
}
#main-window #browser {
grid-row: 9 / 10;
}
#TabsToolbar {
grid-row: 10 / 11;
}
.browser-toolbar {
background: inherit;
background-attachment: fixed;
}
}
Why:
order/-moz-box-ordinal-groupmoves the entire#navigator-toolbox— grid lets each toolbar land in its own row-moz-box-ordinal-groupis legacy XUL layout, actively being removeddisplay: contentson#navigator-toolboxis required so its children participate in the grid
Source: firefox-csshacks/chrome/tabs_below_content_v2.css
Hiding tabs with a single tab
Critical rules beyond just collapsing the tab:
#TabsToolbar {
min-height: 0px !important;
}
#tabbrowser-tabs,
#pinned-tabs-container,
#tabbrowser-arrowscrollbox {
min-height: 0 !important;
}
.accessibility-indicator,
.private-browsing-indicator {
height: unset !important;
}
.accessibility-indicator > hbox {
padding-block: 0 !important;
}
/* Match both :only-of-type AND attribute-based selectors for tab groups/hidden tabs */
.tabbrowser-tab:only-of-type,
.tabbrowser-tab[first-visible-tab="true"][last-visible-tab="true"] {
visibility: collapse !important;
min-height: 0 !important;
height: 0;
}
/* Contain periphery so it can't hold toolbar open */
#tabbrowser-arrowscrollbox-periphery,
#private-browsing-indicator-with-label,
#TabsToolbar > .titlebar-buttonbox-container {
contain: strict;
contain-intrinsic-height: 0px;
}
Why:
:only-of-typealone fails when tabs are hidden by extensions or tab groups —[first-visible-tab][last-visible-tab]covers those cases#pinned-tabs-containercan force minimum height even when all tabs collapse.accessibility-indicator,.private-browsing-indicator,.titlebar-buttonbox-container,#tabbrowser-arrowscrollbox-peripheryall hold the toolbar open withoutcontain: strict- These periphery elements are the most common cause of persistent gaps
Source: firefox-csshacks/chrome/hide_tabs_with_one_tab.css
Removing toolbox borders and separators
Three rules required — setting --chrome-content-separator-color: transparent alone is insufficient:
:root[sizemode="normal"] {
border-top: none !important;
}
#navigator-toolbox::after {
content: none !important;
}
#navigator-toolbox {
border-bottom: none !important;
}
The ::after pseudo-element on #navigator-toolbox draws a bottom border that CSS variables do not control.
Source: firefox-csshacks/chrome/hide_toolbox_top_bottom_borders.css
Tab background direct overrides
CSS variables like --tab-border-radius are not consumed by all internal tab styles. Apply direct overrides:
.tab-background {
border-radius: 0 !important;
box-shadow: none !important;
border-top: 0 !important;
outline: none !important;
}
Source: firefox-csshacks/chrome/non_floating_sharp_tabs.css
Preventing tab animation interference
Firefox's built-in tab animation system can fight visibility collapse:
#TabsToolbar {
will-change: unset !important;
transition: none !important;
opacity: 1 !important;
}
Source: firefox-csshacks/chrome/non_floating_sharp_tabs.css
Autohide entire toolbox
Hide all toolbars, show on hover or urlbar focus. Uses rotateX transform (GPU-accelerated, no layout shift):
#navigator-toolbox {
position: fixed !important;
width: 100vw;
z-index: 10 !important;
transition: transform 82ms ease-in-out 600ms !important;
transform: rotateX(89.9deg);
transform-origin: top;
opacity: 0;
}
#navigator-toolbox:is(:hover, :focus-within) {
transition-delay: 0ms !important;
transform: rotateX(0deg);
opacity: 1;
}
The #urlbar[popover] also needs separate hide/show since it's a popover:
#urlbar[popover] {
opacity: 0;
pointer-events: none;
transition:
transform 82ms ease-in-out 600ms,
opacity 0ms 682ms;
transform: translateY(
calc(0px - var(--tab-min-height) - var(--urlbar-container-height))
);
}
#urlbar-container > #urlbar[popover]:is([focused], [open]) {
opacity: 1;
pointer-events: auto;
transition-delay: 0ms;
transform: translateY(0);
}
Keep toolbox visible when any popup is open:
#mainPopupSet:has(> [panelopen]:not(#tab-preview-panel)) ~ #navigator-toolbox {
transition-delay: 0ms !important;
transform: rotateX(0deg);
opacity: 1;
}
Source: firefox-csshacks/chrome/autohide_toolbox.css
Autohide nav-bar only
Show nav-bar overlaid on tabs when urlbar is focused. Uses grid to stack toolbars:
:root:not([customizing]) #navigator-toolbox {
display: grid;
grid-template-rows: auto;
}
:root:not([customizing]) #navigator-toolbox > .browser-toolbar {
grid-area: 1/1;
}
:root[sessionrestored] #nav-bar:not([customizing]) {
transform: rotateX(89.9deg);
transition:
transform 67ms linear,
opacity 0ms linear 67ms !important;
opacity: 0;
z-index: 3;
}
:root[sessionrestored] #nav-bar:focus-within {
transform: rotateX(0deg);
opacity: 1;
transition-delay: 0ms, 0ms !important;
}
Source: firefox-csshacks/chrome/show_navbar_on_focus_only.css
Autohide tabs toolbar
Hides tabs toolbar, shows on hover. Uses negative margin + transition:
:root {
--uc-tabs-hide-animation-duration: 48ms;
--uc-tabs-hide-animation-delay: 200ms;
}
#TabsToolbar:not([customizing]) {
visibility: hidden;
position: relative;
z-index: 1;
transition:
visibility 0ms linear var(--uc-tabs-hide-animation-delay),
margin-bottom var(--uc-tabs-hide-animation-duration) ease-out
var(--uc-tabs-hide-animation-delay) !important;
}
#navigator-toolbox:has(> :is(#toolbar-menubar, #TabsToolbar):hover)
> #TabsToolbar {
visibility: visible;
margin-bottom: 0px;
transition-delay: 0ms, 0ms !important;
}
Source: firefox-csshacks/chrome/autohide_tabstoolbar_v2.css
Autohide bookmarks toolbar
Uses rotateX with configurable delay:
#PersonalToolbar {
--uc-bm-height: 20px;
--uc-bm-padding: 4px;
--uc-autohide-toolbar-delay: 600ms;
}
#PersonalToolbar:not([customizing]) {
position: relative;
margin-bottom: calc(-1px - var(--uc-bm-height) - 2 * var(--uc-bm-padding));
transform: rotateX(90deg);
transform-origin: top;
transition: transform 135ms linear var(--uc-autohide-toolbar-delay) !important;
z-index: 1;
}
#navigator-toolbox:hover > #PersonalToolbar {
transition-delay: 100ms !important;
transform: rotateX(0);
}
Source: firefox-csshacks/chrome/autohide_bookmarks_toolbar.css
Autohide sidebar
Collapses sidebar to narrow strip, expands on hover:
#sidebar-box {
--uc-sidebar-width: 40px;
--uc-sidebar-hover-width: 210px;
--uc-autohide-sidebar-delay: 600ms;
--uc-autohide-transition-duration: 115ms;
min-width: var(--uc-sidebar-width) !important;
width: var(--uc-sidebar-width) !important;
max-width: var(--uc-sidebar-width) !important;
z-index: 3;
position: relative;
}
#sidebar-header,
#sidebar {
transition: min-width var(--uc-autohide-transition-duration) linear
var(--uc-autohide-sidebar-delay) !important;
min-width: var(--uc-sidebar-width) !important;
will-change: min-width;
}
#sidebar-box:hover > #sidebar-header,
#sidebar-box:hover > #sidebar {
min-width: var(--uc-sidebar-hover-width) !important;
transition-delay: 0ms !important;
}
#sidebar-splitter {
display: none;
}
Source: firefox-csshacks/chrome/autohide_sidebar.css
Hide tabs toolbar entirely
For use with tree-style-tab or native vertical tabs. Moves window controls to nav-bar:
@media not -moz-pref("sidebar.verticalTabs") {
#TabsToolbar:not([customizing]) {
visibility: collapse;
}
:root[customtitlebar]
#toolbar-menubar:is([autohide=""], [autohide="true"])
~ #nav-bar {
> .titlebar-buttonbox-container {
display: flex !important;
}
:root[sizemode="normal"] & {
> .titlebar-spacer {
display: flex !important;
}
}
}
}
@media -moz-pref("sidebar.verticalTabs") {
#sidebar-launcher-splitter,
#sidebar-main {
visibility: collapse;
}
}
Source: firefox-csshacks/chrome/hide_tabs_toolbar_v2.css
Tabs on bottom (within toolbox)
Reorders TabsToolbar below nav-bar without moving below content. Uses order:
@media not -moz-pref("sidebar.verticalTabs") {
.global-notificationbox,
#tab-notification-deck,
#notifications-toolbar,
#TabsToolbar {
order: 1;
}
#TabsToolbar > :is(.titlebar-spacer, .titlebar-buttonbox-container) {
display: none;
}
:root[customtitlebar]
#toolbar-menubar:is([autohide=""], [autohide="true"], [collapsed])
~ #nav-bar {
> .titlebar-buttonbox-container {
display: flex !important;
}
}
}
Source: firefox-csshacks/chrome/tabs_on_bottom_v2.css
All toolbars below content
Moves entire toolbox below content. Uses display: contents + order:
#navigator-toolbox {
display: contents;
--uc-navbar-height: 40px;
}
#main-window > body > #browser,
.global-notificationbox,
#tab-notification-deck,
#notifications-toolbar,
#toolbar-menubar {
order: -1;
}
Urlbar breakout must flip direction when toolbars are below:
#urlbar[breakout][breakout-extend] {
display: flex !important;
flex-direction: column-reverse !important;
transform: translateY(calc(var(--urlbar-container-height) - 100%));
}
.urlbarView-body-inner {
border-top-style: none !important;
}
Source: firefox-csshacks/chrome/toolbars_below_content_v2.css
One-line toolbar (tabs + nav-bar side by side)
Uses CSS grid with fractional columns:
@media not -moz-pref("sidebar.verticalTabs") {
:root:not([chromehidden~="toolbar"]) #navigator-toolbox {
display: grid;
grid-template-columns: 6fr 4fr;
}
#toolbar-menubar,
#PersonalToolbar,
.global-notificationbox {
grid-column: 1/3;
}
#TabsToolbar,
#nav-bar {
grid-row: 2/3;
}
#nav-bar {
border-top: none !important;
}
}
Source: firefox-csshacks/chrome/oneline_toolbar.css
Combined tabs + nav-bar
Tabs share space with nav-bar using flex-wrap:
#navigator-toolbox {
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
}
#nav-bar,
#PersonalToolbar {
flex-grow: 1000;
}
#urlbar-container {
min-width: 250px !important;
}
#urlbar[open]:focus-within {
min-width: var(--uc-urlbar-min-width, none) !important;
}
Source: firefox-csshacks/chrome/combined_tabs_and_main_toolbars.css
Vertical tabs (sidebar-style)
Uses position: absolute on #tabbrowser-tabs with margin on content:
:root:not([customizing]) {
--uc-vertical-tabs-width: 220px;
--uc-navbar-height: 40px;
}
#PersonalToolbar,
#main-window:not([inDOMFullscreen]) > body > #browser {
margin-left: var(--uc-vertical-tabs-width);
}
:root:not([customizing]) #tabbrowser-tabs {
position: absolute !important;
height: 100vh;
left: 0;
padding-top: var(--uc-navbar-height);
width: var(--uc-vertical-tabs-width);
background-color: var(--toolbar-bgcolor);
contain: size;
}
Source: firefox-csshacks/chrome/vertical_tabs.css
Multi-row tabs
Flexbox wrap with scroll:
:root {
--multirow-n-rows: 3;
--multirow-tab-min-width: 100px;
}
#tabbrowser-tabs[orient="horizontal"] {
min-height: unset !important;
flex-wrap: wrap;
overflow-y: auto;
overflow-x: hidden;
max-height: calc(
(var(--tab-min-height) + 2 * var(--tab-block-margin, 0px)) *
var(--multirow-n-rows)
);
scrollbar-width: thin;
scroll-snap-type: y mandatory;
}
.tabbrowser-tab {
scroll-snap-align: start;
}
#tabbrowser-tabs .tabbrowser-tab[pinned] {
position: static !important;
}
Tab reordering does not work with multi-row. Hide scroll buttons and spacer:
#scrollbutton-up,
#scrollbutton-down {
display: none;
}
#tabbrowser-arrowscrollbox > spacer {
display: none !important;
}
Source: firefox-csshacks/chrome/multi-row_tabs.css
Nav-bar below content
Uses position: fixed at bottom. Must use -webkit-box display (not flex) to avoid breaking extension menus:
#browser {
margin-bottom: var(--uc-bottom-toolbar-height, 0px);
}
#nav-bar {
position: fixed !important;
bottom: 0px;
display: -webkit-box;
width: 100%;
z-index: 1;
}
#nav-bar-customization-target {
-webkit-box-flex: 1;
}
Urlbar breakout flips upward:
#urlbar[breakout][breakout-extend] {
display: flex !important;
flex-direction: column-reverse !important;
bottom: 0px !important;
top: auto !important;
}
Source: firefox-csshacks/chrome/navbar_below_content.css
Floating findbar
Overlays findbar at top of content area instead of pushing content down:
findbar {
order: -1;
margin-bottom: -33px;
position: relative;
border-top: none !important;
padding: 0 !important;
background: none !important;
pointer-events: none;
z-index: 1;
}
findbar > .findbar-container,
findbar > .close-icon {
background-color: var(--lwt-accent-color, var(--toolbox-bgcolor)) !important;
pointer-events: auto;
border-bottom-right-radius: 4px;
}
findbar[hidden] {
transform: translateY(-30px);
}
Source: firefox-csshacks/chrome/floating_findbar_on_top.css
Overlay fullscreen toolbars
Toolbars float over content in fullscreen instead of pushing it down:
@media -moz-pref("browser.fullscreen.autohide") {
:root[sizemode="fullscreen"] #navigator-toolbox {
position: fixed !important;
width: 100vw;
z-index: 10 !important;
transition: transform 82ms ease-in-out 600ms !important;
transform: translateY(-100%);
}
:root[sizemode="fullscreen"] #navigator-toolbox:is(:hover, :focus-within) {
transition-delay: 0ms !important;
transform: translateY(0);
}
}
Source: firefox-csshacks/chrome/overlay_fullscreen_toolbars.css
Compact proton
Variables for compact density:
:root {
--toolbarbutton-inner-padding: 6px !important;
--tab-block-margin: 2px !important;
--tabs-shadow-size: 0px !important;
--arrowpanel-menuitem-padding-block: 5px !important;
--panel-font-size: inherit !important;
--arrowpanel-padding: 0.8em !important;
--tab-inline-padding: 8px !important;
}
#nav-bar {
box-shadow: inset 0 var(--tabs-shadow-size) 0 var(--lwt-tabs-border-color) !important;
}
.tab-close-button {
width: 20px !important;
height: 20px !important;
padding: 5px !important;
}
Source: firefox-csshacks/chrome/compact_proton.css
Compact urlbar
Prevents urlbar breakout expansion:
#urlbar[breakout][breakout-extend] {
margin-left: 0 !important;
width: var(--urlbar-width) !important;
margin-top: calc(
(var(--urlbar-container-height) - var(--urlbar-height)) / 2
) !important;
}
:where(#urlbar) > .urlbar-background,
#urlbar-background {
animation: none !important;
}
:where(#urlbar) > .urlbar-input-container {
padding: var(--urlbar-container-padding, 0) 1px !important;
height: var(--urlbar-height) !important;
}
Source: firefox-csshacks/chrome/compact_urlbar_megabar.css
Rounded menupopups
Rounding panels, menus, urlbar consistently:
:root {
--uc-menupopup-border-radius: 20px;
}
panel[type="autocomplete-richlistbox"],
menupopup,
.panel-arrowcontent {
-moz-appearance: none !important;
border-radius: var(--uc-menupopup-border-radius) !important;
overflow: clip !important;
}
/* Match urlbar and searchbar */
searchbar#searchbar,
:where(#urlbar) > .urlbar-background,
#urlbar-background {
border-radius: var(--uc-menupopup-border-radius) !important;
}
/* Fix panel arrow position */
panel[type="arrow"] {
margin-inline-end: calc(-10px - var(--uc-menupopup-border-radius)) !important;
}
.panel-arrow {
margin-inline: var(--uc-menupopup-border-radius) !important;
}
Source: firefox-csshacks/chrome/rounded_menupopups.css
Minimal toolbar buttons
Hide buttons as dots, reveal on hover using transform: scale(0):
toolbar .toolbarbutton-1 > * {
transform: scale(0);
transition: transform 82ms linear !important;
}
toolbar:hover .toolbarbutton-1 > * {
transform: scale(1);
}
/* Dot placeholder */
toolbar .toolbarbutton-1:not([open]) {
background-image: radial-gradient(
circle at center,
currentColor 0,
currentColor 10%,
transparent 15%
);
}
toolbar:hover .toolbarbutton-1 {
background-image: none;
}
Source: firefox-csshacks/chrome/minimal_toolbarbuttons_v3.css
Color variable template
Key variables for full theme override:
:root {
--arrowpanel-background: <color> !important;
--arrowpanel-border-color: <color> !important;
--arrowpanel-color: <color> !important;
--lwt-accent-color: <color> !important;
--toolbar-bgcolor: <color> !important;
--tab-selected-bgcolor: <color> !important;
--lwt-text-color: <color> !important;
--toolbarbutton-icon-fill: <color> !important;
--toolbar-field-background-color: <color> !important;
--toolbar-field-focus-background-color: <color> !important;
--toolbar-field-color: <color> !important;
--toolbar-field-focus-color: <color> !important;
--toolbar-field-border-color: <color> !important;
--toolbar-field-focus-border-color: <color> !important;
--lwt-sidebar-background-color: <color> !important;
--lwt-sidebar-text-color: <color> !important;
}
#navigator-toolbox {
--lwt-tabs-border-color: <color> !important;
}
#tabbrowser-tabs {
--lwt-tab-line-color: <color> !important;
}
#sidebar-box {
--sidebar-background-color: <color> !important;
}
Source: firefox-csshacks/chrome/color_variable_template.css
Linux GTK window controls patch
CSD buttons don't respect layout rules by default on Linux:
.titlebar-buttonbox {
align-items: stretch !important;
}
.titlebar-button {
-moz-appearance: none !important;
-moz-context-properties: fill, stroke, fill-opacity;
fill: currentColor;
padding: 4px 6px !important;
flex-grow: 1;
overflow: clip;
}
Source: firefox-csshacks/chrome/linux_gtk_window_control_patch.css
Window controls on left
Use @media -moz-pref("userchrome.force-window-controls-on-left.enabled"):
@media -moz-pref("userchrome.force-window-controls-on-left.enabled") {
#nav-bar > .titlebar-buttonbox-container {
order: -1 !important;
> .titlebar-buttonbox {
flex-direction: row-reverse;
}
}
}
Also detectable via (-moz-gtk-csd-reversed-placement) or (-moz-platform: macos).
-moz-pref() media queries
Firefox 133+ supports @media -moz-pref("pref.name") for conditional CSS based on about:config prefs. Used extensively by firefox-csshacks for toggleable features:
sidebar.verticalTabs— native vertical tabs enabledbrowser.fullscreen.autohide— fullscreen toolbar autohideuserchrome.*— custom user prefs for CSS feature toggles
Common mistakes
#idvs.class— check source (e.g..private-browsing-indicator-with-labelis a class, not an ID)- Shadow DOM parts (
scrollbutton-up,scrollbutton-down) are::part(), not IDs tabbare element doesn't match — use.tabbrowser-tab- Pseudo-elements (
::before,::after) — verify they exist in source before targeting - Border/separator overrides — find which element has the border (e.g.
.urlbarView-body-inner, not.urlbarView-body-outer) .searchbar-engine-one-off-itemis not inside#urlbar#context-sep-navigationis in browser.xhtml, not in CSS#pocket-button,#scrollbutton-up,#scrollbutton-downdo not exist in browser.xhtmlorder/-moz-box-ordinal-groupfor layout reordering — use CSS grid instead- Setting CSS variables without
!importantdirect overrides — internal styles may not consume the variable - Relying on
visibility: collapsewithout zeroingmin-heighton all child containers - Missing
contain: stricton periphery elements when collapsing toolbars — they hold height open - Adding
!importantto variables — variables are automatically applied,!importanton them is pointless - Duplicate
user_pref()lines in user.js — last one wins silently, remove duplicates - GNOME theme prefs (
gnomeTheme.*) conflict with custom tab positioning