Automatic Material Dark-Mode for YouTube

A low-tech solution to a high-tech problem! Automatically clicks YouTube's "Dark Mode" button if dark mode isn't already active.

Old: v1.2 - 2017-09-13 - Now focuses the search-field after switching to the dark theme, for easier typing
New: v1.3 - 2017-09-13 - Rewrote all code to make the theme switching as fast as your browser/YouTube is capable of! The theme-settings menu is now barely visible before it's closed again. :-)

  • --- /tmp/diffy20250426-3471268-se67m7 2025-04-26 13:55:02.166032372 +0000
  • +++ /tmp/diffy20250426-3471268-c8dtcr 2025-04-26 13:55:02.166032372 +0000
  • @@ -1,7 +1,7 @@
  • // ==UserScript==
  • // @name Automatic Material Dark-Mode for YouTube
  • // @namespace SteveJobzniak
  • -// @version 1.2
  • +// @version 1.3
  • // @description A low-tech solution to a high-tech problem! Automatically clicks YouTube's "Dark Mode" button if dark mode isn't already active.
  • // @author SteveJobzniak
  • // @match *://www.youtube.com/*
  • @@ -15,88 +15,100 @@
  • (function() {
  • 'use strict';
  • - function enableDark() {
  • - // Wait a moment so that the whole page menu is loaded and the "dark mode state" is updated...
  • - var attempt = 0, maxAttempts = 8, waitDelay = 250; // 8*250ms = Max 2 seconds of retries.
  • - var menuWaitTimer = setInterval( function() {
  • - // We must find the "settings" menu, otherwise YouTube hasn't fully loaded yet...
  • - var menuButtons = document.querySelectorAll( 'yt-icon.style-scope.ytd-topbar-menu-button-renderer' );
  • -
  • - // If we've reached max attempts or found success, we must now stop the interval timer.
  • - if( ++attempt >= maxAttempts || menuButtons.length === 2 ) {
  • - clearInterval( menuWaitTimer );
  • - }
  • -
  • - // Failed to find the menu bar. Skip this attempt...
  • - if( menuButtons.length !== 2 ) {
  • - return;
  • - }
  • -
  • - // Check the dark mode state "flag" and only process if dark mode isn't already active.
  • - if( document.body.getAttribute( 'dark' ) !== 'true' ) {
  • - // We MUST open the "settings" menu, otherwise nothing will react to the "toggle dark mode" event!
  • - menuButtons[1].click(); // Click the 2nd menu button.
  • -
  • - // Wait a moment for the settings-menu to open up after clicking...
  • - setTimeout( function() {
  • - // Next, open the "toggle dark mode" settings sub-page.
  • - var subButtons = document.querySelectorAll( 'div#label.style-scope.ytd-toggle-theme-compact-link-renderer' );
  • - if( subButtons.length === 1 ) {
  • - subButtons[0].click(); // Click the "dark mode" sub-page button.
  • + function findElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback ) {
  • + var elems = parentElem.querySelectorAll( elemQuery );
  • + if( elems.length === expectedLength ) {
  • + var item = elems[selectItem];
  • + fnCallback( item );
  • + return true;
  • + }
  • +
  • + //console.log('Debug: Cannot find "'+elemQuery+'".');
  • + return false;
  • + }
  • - // Wait a moment for the sub-page to switch...
  • + function retryFindElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback ) {
  • + // If we can't find the element immediately, we'll perform multiple retries.
  • + var success = findElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback );
  • + if( ! success ) {
  • + var attempt = 0, maxAttempts = 40, waitDelay = 50; // 40 * 50ms = Max ~2 seconds of retries.
  • + var searchTimer = setInterval( function() {
  • + var success = findElement( parentElem, elemQuery, expectedLength, selectItem, fnCallback );
  • +
  • + // If we've reached max attempts or found success, we must now stop the interval timer.
  • + if( ++attempt >= maxAttempts || success ) {
  • + clearInterval( searchTimer );
  • + }
  • + }, waitDelay );
  • + }
  • + }
  • +
  • + function enableDark() {
  • + // Wait until the settings menu is available, to ensure that YouTube's "dark mode state" and code has been loaded...
  • + retryFindElement( document, 'yt-icon.style-scope.ytd-topbar-menu-button-renderer', 2, 1, function( settingsMenuButton ) {
  • + // Check the dark mode state "flag" and abort processing if dark mode is already active.
  • + if( document.body.getAttribute( 'dark' ) === 'true' ) { return; }
  • +
  • + // We MUST open the "settings" menu, otherwise nothing will react to the "toggle dark mode" event!
  • + settingsMenuButton.click();
  • +
  • + // Wait a moment for the settings-menu to open up after clicking...
  • + retryFindElement( document, 'div#label.style-scope.ytd-toggle-theme-compact-link-renderer', 1, 0, function( darkModeSubMenuButton ) {
  • + // Next, go to the "toggle dark mode" settings sub-page.
  • + darkModeSubMenuButton.click();
  • +
  • + // Wait a moment for the settings sub-page to switch...
  • + retryFindElement( document, 'ytd-toggle-item-renderer.style-scope.ytd-multi-page-menu-renderer', 1, 0, function( darkModeSubPageContainer ) {
  • + // Get a reference to the "activate dark mode" button...
  • + retryFindElement( darkModeSubPageContainer, 'paper-toggle-button.style-scope.ytd-toggle-item-renderer', 1, 0, function( darkModeButton ) {
  • + // We MUST now use this very ugly, hardcoded sleep-timer to ensure that YouTube's "activate dark mode" code is fully
  • + // loaded; otherwise, YouTube will be completely BUGGED OUT and WON'T save the fact that we've enabled dark mode!
  • + // Since JavaScript is single-threaded, this timeout simply ensures that we'll leave our current code so that we allow
  • + // YouTube's event handlers to deal with loading the settings-page, and then the timeout gives control back to us.
  • +
  • setTimeout( function() {
  • - // Get a reference to the "toggle dark mode" settings sub-page. This only works if we opened the "dark mode" sub-page.
  • - var toggleMenuElem = document.querySelectorAll( 'ytd-toggle-item-renderer.style-scope.ytd-multi-page-menu-renderer' );
  • - toggleMenuElem = (toggleMenuElem.length === 1 ? toggleMenuElem[0] : undefined); // Always missing unless we visit the page...
  • -
  • - if( toggleMenuElem ) {
  • - // Get a reference to the "activate dark mode" button.
  • - var darkModeButton = toggleMenuElem.querySelectorAll( 'paper-toggle-button.style-scope.ytd-toggle-item-renderer' );
  • - darkModeButton = (darkModeButton.length === 1 ? darkModeButton[0] : undefined);
  • -
  • - // Now simply click the button to enable dark mode.
  • - darkModeButton.click();
  • -
  • - // Give keyboard focus to the input serach field.
  • - setTimeout( function() {
  • - var searchField = document.querySelectorAll( 'input#search' );
  • - if( searchField.length === 1 ) {
  • - // Clicking the searchfield first is just done to hide the settings-panel.
  • - searchField[0].click();
  • - searchField[0].focus();
  • - }
  • - }, 150 );
  • - }
  • -
  • - // Alternative method, which switches using an event instead of clicking the button...
  • - // I decided to disable this method, since it relies on intricate internal details...
  • - // and requires the menu to be open to work anyway, so we may as well just click it...
  • - /*
  • - var ytDebugMenu = document.querySelectorAll('ytd-debug-menu');
  • - ytDebugMenu = (ytDebugMenu.length === 1 ? ytDebugMenu[0] : undefined);
  • - if( ytDebugMenu ) {
  • - ytDebugMenu.fire(
  • - 'yt-action',
  • - {
  • - actionName:'yt-signal-action-toggle-dark-theme-on',
  • - optionalAction:false,
  • - args:[
  • - {signalAction:{signal:'TOGGLE_DARK_THEME_ON'}},
  • - toggleMenuElem,
  • - undefined
  • - ],
  • - returnValue: []
  • - },
  • - {}
  • - );
  • - }
  • - */
  • - }, 250 ); // Delay to wait for the dark mode settings subpage to open.
  • - }
  • - }, 500 ); // Delay to wait for the settings menu to fully open.
  • - }
  • - }, waitDelay ); // Check-interval after page load to ensure the deferred menu topbar and dark mode state are fully loaded.
  • + // Now simply click YouTube's button to enable their dark mode.
  • + darkModeButton.click();
  • +
  • + // And lastly, give keyboard focus back to the input search field... (We don't need any setTimeout here...)
  • + retryFindElement( document, 'input#search', 1, 0, function( searchField ) {
  • + searchField.click(); // First, click the search-field to force the settings-panel to close...
  • + searchField.focus(); // ...and finally give the search-field focus! Voila!
  • + } );
  • + }, 20 ); // We can use 0ms here for "as soon as possible" instead, but our "at least 20ms" might be safer just in case.
  • + } );
  • + } );
  • + } );
  • + } );
  • +
  • + // Alternative method, which switches using an internal YouTube event instead of clicking
  • + // the menus... I decided to disable this method, since it relies on intricate internal
  • + // details, and it still requires their menu to be open to work anyway (because their
  • + // code for changing theme isn't active until the Dark Mode settings menu is open),
  • + // so we may as well just click the actual menu items. ;-)
  • + /*
  • + var ytDebugMenu = document.querySelectorAll('ytd-debug-menu');
  • + ytDebugMenu = (ytDebugMenu.length === 1 ? ytDebugMenu[0] : undefined);
  • + if( ytDebugMenu ) {
  • + ytDebugMenu.fire(
  • + 'yt-action',
  • + {
  • + actionName:'yt-signal-action-toggle-dark-theme-on',
  • + optionalAction:false,
  • + args:[
  • + {signalAction:{signal:'TOGGLE_DARK_THEME_ON'}},
  • + toggleMenuElem,
  • + undefined
  • + ],
  • + returnValue: []
  • + },
  • + {}
  • + );
  • + }
  • + */
  • +
  • + // Also note that it may be possible to simply modify the YouTube cookies, by changing
  • + // "PREF=f1=50000000;" to "PREF=f1=50000000&f6=400;" (dark mode on) and then reloading the page.
  • + // However, a reload is always slower than toggling the settings menu, so I didn't do that.
  • }
  • if( document.readyState === 'complete' ) {