Skip to content

Instantly share code, notes, and snippets.

@Bouke
Last active June 9, 2026 06:53
Show Gist options
  • Select an option

  • Save Bouke/69bdff2dbeb4b417b826ea693c721dce to your computer and use it in GitHub Desktop.

Select an option

Save Bouke/69bdff2dbeb4b417b826ea693c721dce to your computer and use it in GitHub Desktop.
Detects system theme changes and selects the appropriate theme on Azure DevOps.
// ==UserScript==
// @name Theme Change Detector
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Keeps the Azure DevOps theme in sync with the system color scheme, reconciling whenever ADO applies or changes the theme.
// @author Bouke Haarsma
// @match https://dev.azure.com/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
let applying = false;
function clickElement(selector) {
const element = document.querySelector(selector);
if (element) {
element.click();
}
}
async function waitForElement(selector) {
while (!document.querySelector(selector)) {
await new Promise(resolve => setTimeout(resolve, 10));
}
return document.querySelector(selector);
}
async function handleThemeChange(isDarkMode) {
clickElement('button[aria-label="User settings"]');
const themeLink = await waitForElement('#__bolt-changeThemeLink-text');
themeLink.click();
if (isDarkMode) {
const darkThemeButton = await waitForElement('#theme-ms-vss-web-vsts-theme-dark');
darkThemeButton.click();
} else {
const lightThemeButton = await waitForElement('#theme-ms-vss-web-vsts-theme');
lightThemeButton.click();
}
const closeButton = await waitForElement('.theme-panel button[aria-label="Close"]');
closeButton.click();
}
function getCurrentTheme() {
const body = document.body;
if (body.hasAttribute('data-theme')) {
return body.getAttribute('data-theme') === 'ms.vss-web.vsts-theme-dark' ? 'dark' : 'light';
} else {
return body.classList.contains('ms-vss-web-vsts-theme-dark') ? 'dark' : 'light';
}
}
function desiredTheme() {
return mediaQueryList.matches ? 'dark' : 'light';
}
// Reconcile ADO's theme towards the system preference. Runs whenever the
// body theme changes (including ADO applying its stored theme during boot)
// and whenever the system preference changes. The `applying` guard ignores
// the body mutations caused by our own menu clicks so we don't loop.
async function reconcile() {
if (applying) return;
if (getCurrentTheme() === desiredTheme()) return;
applying = true;
try {
await handleThemeChange(desiredTheme() === 'dark');
} finally {
applying = false;
}
}
async function waitForBody() {
while (!document.body) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
async function waitForThemeApplied() {
while (!document.body.hasAttribute('data-theme') &&
!/vsts-theme/.test(document.body.className)) {
await new Promise(resolve => setTimeout(resolve, 50));
}
}
(async function init() {
await waitForBody();
// React to ADO applying or later changing the theme on <body>.
const observer = new MutationObserver(reconcile);
observer.observe(document.body, { attributes: true, attributeFilter: ['data-theme', 'class'] });
// React to system theme changes (addEventListener replaces the
// deprecated addListener).
mediaQueryList.addEventListener('change', reconcile);
// Don't trust getCurrentTheme() until ADO has actually applied a theme.
await waitForThemeApplied();
reconcile();
})();
})();
@Bouke

Bouke commented Jun 9, 2026

Copy link
Copy Markdown
Author

0.2 rewritten by claude to better handle theme mismatch on page load:

Theme Change Detector 0.2

  • Fixed the theme not syncing on page load — the script now waits for Azure DevOps to apply its theme instead of checking once and racing the SPA boot.
  • Switched to a MutationObserver that reconciles the theme whenever ADO applies or later changes it, and self-corrects if ADO overrides your system preference.
  • Now runs at document-start so it's watching before the theme is set.
  • React to system light/dark changes via addEventListener (replaces the deprecated addListener).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment