Writing your own .uc.js
A .uc.js script is ordinary JavaScript that runs inside a Firefox chrome window (the window
that holds the tab strip, the toolbar and the menus). That is the whole idea: script the browser's
own interface.
Who is this page for
Section titled “Who is this page for”Two kinds of readers land here. If you just want to use someone else's script, you need
three steps and no programming: save the .uc.js file into the chrome folder in your profile
folder, restart Firefox, done. What that folder is lives on the userChrome.js
page. If you want to write your own, read on: a few
lines of JavaScript plus a small header block are the whole skill, and the tutorial below builds
it up piece by piece.
Your first script
Section titled “Your first script”- Find the profile. Open
about:supportand read Profile Folder. - Open the
chromedirectory inside it, creating it if it does not exist. - Create
hello.uc.jsnext to youruserChrome.css. The loader only scans the top level ofchrome/, not subfolders. - Write the script below, then restart the browser.
// ==UserScript==// @name Hello from a user script// @include main// ==/UserScript==
Services.obs.addObserver((doc) => { const button = doc.createXULElement("toolbarbutton"); button.setAttribute("label", "Hello"); button.setAttribute("tooltiptext", "Hello from hello.uc.js"); button.addEventListener("command", () => alert("It works.")); doc.getElementById("nav-bar")?.appendChild(button);}, "chrome-document-loaded");The header keys
Section titled “The header keys”Everything is read from the // ==UserScript== block. Unknown keys are ignored, so extra metadata
is harmless.
| Key | Meaning |
|---|---|
@name |
display name (also used as the default id) |
@include |
windows to run in; repeatable. main = the browser window. No @include means main |
@exclude |
windows to skip; repeatable |
@version, @description, @author |
metadata, shown by scripts that list user scripts |
@id |
stable identifier; defaults to the file name and author, e.g. my-script.uc.js@me |
@homepageURL, @downloadURL, @updateURL |
where the script came from (a checker script can use them) |
@optionsURL |
an options page for the script |
@onlyonce |
run a single time per session, even though windows come and go |
@startup / @shutdown |
functions called after load / before unload |
@include and @exclude are matched against the window's URL as a regular expression, where *
is a wildcard and main expands to chrome://browser/content/browser.xhtml:
// @include *://*.example.com/* ← every window whose URL matches// @include main ← the browser window// @exclude about:privatebrowsingGlobals available in a script
Section titled “Globals available in a script”Chrome code runs with the full XPCOM API. This is privileged context, not a web page:
| Global | What it gives you |
|---|---|
Services |
prefs, observers, the directory service, startup (everything in XUL) |
Components / Cc / Ci |
component creation and the interface constants |
ChromeUtils |
importESModule, defineESModuleGetters, defineLazyGetter |
Cu |
Cu.Sandbox / Cu.evalInSandbox (see userChrome.js) |
gBrowser |
the browser's tab strip: addTab, selectedTab, selectedTabs |
document, window |
the current chrome document and window |
UC, _uc |
the loader's registry and state |
The restart.uc.js
script is a good reference. It lives in the repository's core/chrome/ tree and adds a toolbar
button through CustomizableUI.createWidget(), registers a chrome-document-loaded observer, and
restarts the application with Services.startup.quit(...). It is not necessarily shipped inside
utils.zip, so treat the repo file, not the installed copy, as the authoritative example.
Adding a button to the toolbar
Section titled “Adding a button to the toolbar”Two approaches. The modern one registers a real toolbar widget:
const { CustomizableUI } = ChromeUtils.importESModule( "resource:///modules/CustomizableUI.sys.mjs",);
CustomizableUI.createWidget({ id: "my-widget", type: "custom", defaultArea: CustomizableUI.AREA_NAVBAR, onBuild(doc) { const button = doc.createXULElement("toolbarbutton"); button.setAttribute("label", "Do it"); button.addEventListener("command", () => doSomething()); return button; },});The classic one appends to an existing element:
Services.obs.addObserver((doc) => { if (doc.documentURI !== "chrome://browser/content/browser.xhtml") return; doc.getElementById("nav-bar")?.appendChild(button);}, "chrome-document-loaded");Disabling a script without deleting it
Section titled “Disabling a script without deleting it”Add its file name to the userChromeJS.scriptsDisabled preference in about:config (comma
separated), or set userChromeJS.enabled to false to suspend all of them at once. The file stays
on disk, ready to re-enable.
Debugging a script
Section titled “Debugging a script”Errors in a .uc.js land in the Browser Console.
- Enable it:
about:debugging#/runtime/this-firefox→ Inspect, ordevtools.console.stdout.contentinabout:configfor the remote console. - Restart the browser; the console shows the loader's output and any error, including a one-line stack pointing at your file.
- File extension exactly
.uc.js(case-insensitive match, but keep it lowercase). - The file is in
chrome/, not inchrome/utils/. - The
// ==UserScript==header is complete (// ==/UserScript==closes it). @includematches the window you are testing in. No@includemeansmain.- Startup cache cleared after every edit.
// what does the loader think it found?ChromeUtils.reportError(JSON.stringify(_uc.scripts, null, 2));_uc.scripts holds every discovered script with its parsed metadata, so a missing @include or a
mis-parsed header shows up immediately. The loader's own object is also exposed as UC on every
chrome window.
Before you publish a script
Section titled “Before you publish a script”- Keep the source somewhere. The profile folder is not a good place to keep the only copy.
- Say where it came from.
@homepageURL/@updateURLlet other people's scripts find your releases. - Version it.
@versionplus a@downloadURLpointing at the file is what a checker script compares against.