Hybrid dark mode across Google's web apps. Auto-detects native dark theme per page and falls back to whole-page filter inversion when native dark is unavailable or absent on the active account.
A Tampermonkey userscript that delivers true dark mode across Google's web apps in two complementary modes, with automatic detection of which one to use:
filter: invert(1) hue-rotate(180deg). Catches every surface, including canvas-rendered content.The script reads the page's body background luminance after first paint to decide which mode to apply, so multi-account users automatically get the right behavior on each account without any configuration.
Silent, always-on, no UI.
GM_addStyle).darkmode.user.js → Save.Always filter mode (canvas-rendered or no usable native dark): | App | URL | |---|---| | Docs | docs.google.com/document | | Sheets | docs.google.com/spreadsheets, sheets.google.com | | Slides | docs.google.com/presentation | | Forms | docs.google.com/forms | | Vids | docs.google.com/videos | | Drawings | docs.google.com/drawings | | Apps Script | script.google.com |
Auto-detected (patch mode if native dark is on, filter mode otherwise):
| Group | Apps |
|---|---|
| Workspace | Gmail, Calendar, Drive, Keep, Meet, Chat, Voice, Sites, Contacts, Photos, Classroom, Translate, Workspace Admin, Groups |
| Developer / admin | Gemini, AI Studio, Cloud Console, Firebase Console, Search Console, Looker Studio, Analytics |
| Ads / commerce | Google Ads, AdSense, Merchant Center |
| Search / info | Google Search results, Trends, Scholar, News |
A single rule applies filter: invert(1) hue-rotate(180deg) to <html>. Every pixel rendered on the page is inverted, including content drawn inside <canvas> (which CSS otherwise can't reach — modern Docs and Sheets render their main editing surface as canvas).
To stop photos and videos from appearing as negatives, the script re-inverts media:
<img>, <video>, <iframe>, <embed>, <object><image> elements inside SVG (Slides and Docs render embedded photos this way)background-image stylesFilter mode preserves original colors of: photos, embedded videos, chart images, slide images, screenshots — anything rendered as a media element.
Filter mode shifts:
This is the cost of catching every surface without per-class CSS maintenance. Google's class names are heavily obfuscated and change across deploys, so per-class targeting is not practical for true comprehensive coverage.
Tampermonkey dashboard → toggle the script off, or delete it.
A specific image looks inverted on a filter-mode app. It's probably rendered through a path the script doesn't yet recognize. Open DevTools, find the element, note its tag and class. Add a re-invert rule to FILTER_BASE in darkmode.user.js:
your.selector { filter: invert(1) hue-rotate(180deg) !important; }
A patch-mode surface (Gmail/Calendar/Drive dialog, popover) is still light. Google likely changed a class name. Open the element in DevTools, find a stable selector (prefer [role=...] or [aria-label=...] over generated classes), and add a rule to the relevant MODULES.gmail / MODULES.calendar / MODULES.drive block.
Disable one app while keeping others working. Each app's CSS is its own module string inside darkmode.user.js (MODULES.gmail, MODULES.calendar, etc.). Set the offending one to an empty string ` ` and reload.
Disable filter mode for an app. In dispatch(), change the app's classification from 'filter' to 'none'.
darkmode.user.js — the userscriptdocs/superpowers/specs/2026-05-04-google-dark-mode-userscript-design.md — original design (note: the design has since pivoted from per-class theming to filter mode for Docs/Sheets/Apps Script, see commit history)docs/superpowers/plans/2026-05-04-google-dark-mode-userscript.md — original implementation plan