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 behind browser.urlbar.scotchBonnet.enableOverride or browser.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-group moves the entire #navigator-toolbox — grid lets each toolbar land in its own row
-
-moz-box-ordinal-group is legacy XUL layout, actively being removed
-
display: contents on #navigator-toolbox is 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-type alone fails when tabs are hidden by extensions or tab groups — [first-visible-tab][last-visible-tab] covers those cases
-
#pinned-tabs-container can force minimum height even when all tabs collapse
-
.accessibility-indicator , .private-browsing-indicator , .titlebar-buttonbox-container , #tabbrowser-arrowscrollbox-periphery all hold the toolbar open without contain: 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 enabled
-
browser.fullscreen.autohide — fullscreen toolbar autohide
-
userchrome.* — custom user prefs for CSS feature toggles
Common mistakes
-
#id vs .class — check source (e.g. .private-browsing-indicator-with-label is a class, not an ID)
-
Shadow DOM parts (scrollbutton-up , scrollbutton-down ) are ::part() , not IDs
-
tab bare 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-item is not inside #urlbar
-
#context-sep-navigation is in browser.xhtml, not in CSS
-
#pocket-button , #scrollbutton-up , #scrollbutton-down do not exist in browser.xhtml
-
order / -moz-box-ordinal-group for layout reordering — use CSS grid instead
-
Setting CSS variables without !important direct overrides — internal styles may not consume the variable
-
Relying on visibility: collapse without zeroing min-height on all child containers
-
Missing contain: strict on periphery elements when collapsing toolbars — they hold height open
-
Adding !important to variables — variables are automatically applied, !important on 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