Last active
June 9, 2026 06:53
-
-
Save Bouke/69bdff2dbeb4b417b826ea693c721dce to your computer and use it in GitHub Desktop.
Detects system theme changes and selects the appropriate theme on Azure DevOps.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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(); | |
| })(); | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
0.2 rewritten by claude to better handle theme mismatch on page load: