Skip to content

Instantly share code, notes, and snippets.

@vdeemann
Created May 22, 2026 20:37
Show Gist options
  • Select an option

  • Save vdeemann/74cf325e6f7d29f5a51989bbf99cb90a to your computer and use it in GitHub Desktop.

Select an option

Save vdeemann/74cf325e6f7d29f5a51989bbf99cb90a to your computer and use it in GitHub Desktop.
In theater mode: title, Subscribe row, and description are centered as a group at the top with empty space on either side. Recommendations sidebar appears on the right starting at the comments section. No flicker on page load.
// ==UserScript==
// @name YouTube Theater Mode - Centered Top + Sidebar Beside Comments
// @namespace https://github.com/vdeemann
// @version 9.0.0
// @description In theater mode: title, Subscribe row, and description are centered as a group at the top with empty space on either side. Recommendations sidebar appears on the right starting at the comments section. No flicker on page load.
// @author Dee
// @match https://www.youtube.com/*
// @match https://youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const STYLE_ID = 'yt-theater-centered-v9';
const READY_CLASS = 'yt-theater-sidebar-ready';
// Width of the top centered content group (title, subscribe row, description).
const CENTERED_WIDTH = 1280;
// Width of the right-side recommendations column.
const SIDEBAR_WIDTH = 402;
// Gap between comments column and sidebar.
const COLUMN_GAP = 24;
// Tweak: + lowers the sidebar, - raises it.
const ALIGN_FUDGE = 0;
const css = `
:root {
--yt-theater-sidebar-top: 0px;
}
/* ============================================================
HIDE-UNTIL-READY: kills flicker on initial load and on SPA nav.
Selectors target #secondary and related containers by plain ID
so they match before custom elements upgrade.
============================================================ */
html:not(.${READY_CLASS}) #secondary,
html:not(.${READY_CLASS}) #secondary-inner,
html:not(.${READY_CLASS}) ytd-watch-next-secondary-results-renderer,
html:not(.${READY_CLASS}) #related {
display: none !important;
}
/* ============================================================
THEATER MODE ONLY: the rules below scope themselves to
ytd-watch-flexy[theater] so default mode is untouched.
============================================================ */
/* The columns container must be a positioning context so we can
absolutely position #secondary relative to it. */
ytd-watch-flexy[theater] #columns.ytd-watch-flexy {
position: relative !important;
}
/* Constrain and center the content ABOVE comments. These are the
direct children of #primary that hold title, owner/subscribe row,
description, and the chip bar. */
ytd-watch-flexy[theater] #primary.ytd-watch-flexy > #primary-inner > ytd-watch-metadata,
ytd-watch-flexy[theater] #primary.ytd-watch-flexy > #primary-inner > #below,
ytd-watch-flexy[theater] #primary.ytd-watch-flexy > #primary-inner > #info,
ytd-watch-flexy[theater] #primary.ytd-watch-flexy ytd-watch-metadata {
max-width: ${CENTERED_WIDTH}px !important;
margin-left: auto !important;
margin-right: auto !important;
box-sizing: border-box !important;
}
/* Sidebar: absolutely positioned to the right of #columns, top set
by JS-measured CSS variable. */
html.${READY_CLASS} ytd-watch-flexy[theater] #secondary.ytd-watch-flexy {
display: block !important;
position: absolute !important;
top: var(--yt-theater-sidebar-top) !important;
right: 24px !important;
width: ${SIDEBAR_WIDTH}px !important;
max-width: ${SIDEBAR_WIDTH}px !important;
min-width: ${SIDEBAR_WIDTH}px !important;
margin: 0 !important;
padding: 0 !important;
opacity: 1 !important;
z-index: 1 !important;
}
ytd-watch-flexy[theater] #secondary-inner.ytd-watch-flexy {
width: 100% !important;
max-width: 100% !important;
position: static !important;
}
/* Make sure related items render normally inside the sidebar. */
html.${READY_CLASS} ytd-watch-flexy[theater] #related.ytd-watch-flexy,
html.${READY_CLASS} ytd-watch-flexy[theater] ytd-watch-next-secondary-results-renderer {
display: block !important;
visibility: visible !important;
width: 100% !important;
max-width: 100% !important;
}
ytd-watch-flexy[theater] #related.ytd-watch-flexy #items {
display: block !important;
}
ytd-watch-flexy[theater] #related.ytd-watch-flexy ytd-compact-video-renderer,
ytd-watch-flexy[theater] #related.ytd-watch-flexy ytd-compact-radio-renderer,
ytd-watch-flexy[theater] #related.ytd-watch-flexy ytd-compact-playlist-renderer {
display: flex !important;
width: 100% !important;
max-width: 100% !important;
margin: 0 0 8px 0 !important;
}
/* Reserve right-side space for the absolutely positioned sidebar
in the comments area, so comments don't extend under it. */
ytd-watch-flexy[theater] ytd-comments#comments,
ytd-watch-flexy[theater] #primary > #primary-inner > ytd-comments,
ytd-watch-flexy[theater] #primary ytd-comments {
padding-right: ${SIDEBAR_WIDTH + COLUMN_GAP}px !important;
box-sizing: border-box !important;
}
`;
function injectStyle() {
// Remove styles from any prior versions of this script.
[
'yt-theater-relocate-recommendations',
'yt-theater-keep-sidebar',
'yt-theater-sidebar-below',
'yt-theater-sidebar-aligned',
'yt-theater-centered-top'
].forEach(id => {
const el = document.getElementById(id);
if (el) el.remove();
});
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
function markReady() {
document.documentElement.classList.add(READY_CLASS);
}
function markNotReady() {
document.documentElement.classList.remove(READY_CLASS);
}
function findCommentsAnchor() {
return (
document.querySelector('ytd-comments#comments') ||
document.querySelector('#comments') ||
document.querySelector('ytd-comments')
);
}
function isOnWatchPage() {
return /\/watch\b/.test(location.pathname);
}
/**
* Measure where comments sit relative to #columns and set the sidebar's
* top position so its top edge aligns with the comments start.
*/
function updateSidebarOffset() {
if (!isOnWatchPage()) {
markReady();
return true;
}
const flexy = document.querySelector('ytd-watch-flexy');
if (!flexy) return false;
if (!flexy.hasAttribute('theater')) {
markReady();
return true;
}
const columns = flexy.querySelector('#columns');
const comments = findCommentsAnchor();
if (!columns || !comments) return false;
const colRect = columns.getBoundingClientRect();
const cmtRect = comments.getBoundingClientRect();
if (cmtRect.height === 0 && cmtRect.top === 0) {
return false;
}
const top = Math.max(0, Math.round(cmtRect.top - colRect.top + ALIGN_FUDGE));
document.documentElement.style.setProperty('--yt-theater-sidebar-top', `${top}px`);
markReady();
return true;
}
let scheduled = false;
function scheduleUpdate() {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
updateSidebarOffset();
});
}
function onNavigateStart() {
markNotReady();
}
function onNavigateFinish() {
injectStyle();
startMeasurementLoop();
}
let loopActive = false;
function startMeasurementLoop() {
if (loopActive) return;
loopActive = true;
let tries = 0;
const maxTries = 900; // ~15s at 60fps
const tick = () => {
if (updateSidebarOffset()) {
loopActive = false;
return;
}
if (++tries < maxTries) {
requestAnimationFrame(tick);
} else {
loopActive = false;
// Safety reveal so user isn't stuck with an invisible sidebar.
markReady();
}
};
requestAnimationFrame(tick);
}
function setupObservers() {
window.addEventListener('resize', scheduleUpdate);
window.addEventListener('yt-navigate-start', onNavigateStart);
window.addEventListener('yt-navigate-finish', onNavigateFinish);
window.addEventListener('yt-page-data-updated', scheduleUpdate);
const observeFlexy = () => {
const flexy = document.querySelector('ytd-watch-flexy');
if (!flexy || flexy.__ytTheaterObserved) return;
flexy.__ytTheaterObserved = true;
new MutationObserver(scheduleUpdate).observe(flexy, {
attributes: true,
attributeFilter: ['theater', 'fullscreen', 'hidden']
});
const primary = flexy.querySelector('#primary');
if (primary && typeof ResizeObserver !== 'undefined') {
new ResizeObserver(scheduleUpdate).observe(primary);
}
};
observeFlexy();
if (!document.querySelector('ytd-watch-flexy') && document.body) {
const bodyObs = new MutationObserver(() => {
if (document.querySelector('ytd-watch-flexy')) {
observeFlexy();
bodyObs.disconnect();
}
});
bodyObs.observe(document.body, { childList: true, subtree: true });
}
}
// ---- Boot ----
injectStyle();
if (!document.head) {
const headWatch = new MutationObserver(() => {
if (document.head) {
injectStyle();
headWatch.disconnect();
}
});
headWatch.observe(document.documentElement, { childList: true });
}
function boot() {
injectStyle();
setupObservers();
startMeasurementLoop();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot, { once: true });
} else {
boot();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment