Skip to content

Instantly share code, notes, and snippets.

@RodriMora
Created November 2, 2025 19:25
Show Gist options
  • Select an option

  • Save RodriMora/280c3edde75d0fd99a27f5e4c42622bf to your computer and use it in GitHub Desktop.

Select an option

Save RodriMora/280c3edde75d0fd99a27f5e4c42622bf to your computer and use it in GitHub Desktop.
Tampermonkey script to automatically switch to theater mode when the browser window is half size
// ==UserScript==
// @name YouTube Auto-Theater by Window Width (robust)
// @namespace rodrigo.mora.autotheater
// @version 0.2
// @description Toggle Theater mode automatically based on browser window width; works on reloads + SPA navigations.
// @match https://www.youtube.com/*
// @exclude https://www.youtube.com/embed/*
// @run-at document-idle
// @grant none
// @noframes
// ==/UserScript==
(() => {
// --- Tweakables ---
const WIDTH_FRACTION = 0.62; // enter Theater when window.innerWidth <= 62% of screen width
const MIN_THRESHOLD_PX = 1000; // never go below this cutoff
const RESIZE_DEBOUNCE_MS = 120; // how often we react to window resizes
const BURST_INTERVAL_MS = 250; // how often we "burst" enforce after nav/load
const BURST_DURATION_MS = 5000; // how long we burst enforce after nav/load
let lastDesired = undefined;
let burstTimer = null;
let flexyObserver = null;
// --- Helpers ---
const isWatchPage = () => location.pathname === '/watch';
const flexy = () => document.querySelector('ytd-watch-flexy');
const isTheater = () => !!document.querySelector('ytd-watch-flexy[theater]');
const isFullscreen = () =>
!!document.querySelector('ytd-watch-flexy[fullscreen]') || !!document.fullscreenElement;
const thresholdPx = () => {
const base = window.screen?.availWidth || window.screen?.width || window.innerWidth;
return Math.max(MIN_THRESHOLD_PX, Math.round(base * WIDTH_FRACTION));
};
const desireTheater = () => window.innerWidth <= thresholdPx();
const clickTheaterButton = () => {
const btn =
document.querySelector('.ytp-size-button') ||
document.querySelector('button[aria-label*="Theater"]');
if (btn) {
btn.click();
return true;
}
return false;
};
const sendKeyT = () => {
const opts = { key: 't', code: 'KeyT', keyCode: 84, which: 84, bubbles: true };
document.dispatchEvent(new KeyboardEvent('keydown', opts));
document.dispatchEvent(new KeyboardEvent('keyup', opts));
};
const toggleTheaterViaUI = () => clickTheaterButton() || sendKeyT();
const enforce = () => {
if (!isWatchPage() || isFullscreen()) return;
const f = flexy();
if (!f) return; // wait until the player container exists
const want = desireTheater();
if (want === lastDesired && isTheater() === want) return; // nothing to do
// Update lastDesired before toggling so burst calls won’t loop
lastDesired = want;
if (isTheater() !== want) toggleTheaterViaUI();
};
const debounce = (fn, ms) => {
let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
};
const startBurst = () => {
if (burstTimer) clearInterval(burstTimer);
const t0 = Date.now();
burstTimer = setInterval(() => {
enforce();
if (Date.now() - t0 > BURST_DURATION_MS) {
clearInterval(burstTimer);
burstTimer = null;
}
}, BURST_INTERVAL_MS);
};
const observeFlexy = () => {
const f = flexy();
if (!f) return;
if (flexyObserver) { try { flexyObserver.disconnect(); } catch {}
}
flexyObserver = new MutationObserver(debounce(enforce, 50));
flexyObserver.observe(f, {
attributes: true,
attributeFilter: ['theater', 'fullscreen', 'style'],
childList: true,
subtree: true,
});
};
const onNavOrLoad = () => {
lastDesired = undefined; // force re-evaluation
enforce();
observeFlexy();
startBurst(); // catch late-mounting controls/layout
};
const init = () => {
onNavOrLoad();
// SPA navigations on youtube.com
window.addEventListener('yt-navigate-finish', onNavOrLoad, { passive: true });
// Another SPA signal YouTube emits when content changes
document.addEventListener('yt-page-data-updated', onNavOrLoad, { passive: true });
// Handle normal loads/refresh + BFCache restores
window.addEventListener('load', onNavOrLoad, { passive: true });
window.addEventListener('pageshow', onNavOrLoad, { passive: true });
// When tab regains focus after being resized elsewhere
document.addEventListener('visibilitychange', () => {
if (!document.hidden) onNavOrLoad();
}, { passive: true });
// Resizes and fullscreen changes
window.addEventListener('resize', debounce(enforce, RESIZE_DEBOUNCE_MS));
document.addEventListener('fullscreenchange', enforce);
// Whole-document observer: if YouTube shuffles big chunks, re-check
const mo = new MutationObserver(debounce(() => {
observeFlexy();
enforce();
}, 100));
mo.observe(document.documentElement, { childList: true, subtree: true });
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment