Hide YT shorts and Mixes and partially watched vids and remove the split-scroll situation
// ==UserScript==
// @name CleanerYT
// @namespace https://www.twitch.tv/simplevar
// @version 2026-04-30
// @description Hide YT shorts and Mixes and partially watched vids and remove the split-scroll situation
// @author SimpleVar
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant none
// @run-at document-body
// @license The Unlicensed
// ==/UserScript==
(() => {
function waitTruthy(pollInterval, fn) {
return new Promise((res, _) => {
poll()
function poll() {
const x = fn()
if (x) res(x)
else setTimeout(poll, pollInterval)
}
})
}
async function waitEl(elOrSelector, predicate = undefined, pollInterval = 100, knownParent = undefined) {
if (!(elOrSelector instanceof HTMLElement)) {
knownParent ??= document
elOrSelector = await waitTruthy(pollInterval, () => knownParent.querySelector(elOrSelector))
}
if (predicate) await waitTruthy(pollInterval, () => predicate(elOrSelector))
return elOrSelector
}
function addStyle(rules) {
let s = document.createElement('style')
s.innerText = rules.replaceAll('\r', '').replaceAll('\n', ' ')
document.head.appendChild(s)
return s
}
addStyle(`
ytd-reel-shelf-renderer { display: none !important; } /* ew */
ytd-rich-section-renderer { display: none !important; }
`)
const styleMixes = addStyle(`
ytd-rich-item-renderer { display: none; } /* nothing gets through, we white listing things that aren't mixes by having an 'avatar' */
ytd-rich-item-renderer:has(yt-lockup-metadata-view-model > .ytLockupMetadataViewModelAvatar) { display: block !important; } /* no mixes */
ytd-rich-item-renderer:has(.ytLockupMetadataViewModelHeadingReset[title^="Mix - "]) { display: none !important; } /* no mixes */
ytd-browse[page-subtype="channels"] ytd-rich-item-renderer { display: block !important; }
`)
const styleWatchPercents = new Array(100).fill().map((x,i) => addStyle(`
ytd-rich-item-renderer:has(.ytThumbnailOverlayProgressBarHostWatchedProgressBarSegment[style="width: ${i+1}%;"]),
ytd-watch-next-secondary-results-renderer yt-lockup-view-model:has(.ytThumbnailOverlayProgressBarHostWatchedProgressBarSegment[style="width: ${i+1}%;"])
{ display: none !important; }
`))
function styleOn(s) {
console.log('styleOn', s)
document.head.appendChild(s)
}
function styleOff(s) {
console.log('styleOff', s)
s.remove()
}
let used_styleWatchPercents = 0
setTimeout(async () => {
const container = await waitEl('#masthead > #container > #start')
const div = document.createElement('div')
container.appendChild(div)
div.style = 'display: flex; flex: 1 0; flex-direction: row;'
{
const x = document.createElement('div')
x.style = 'flex-direction: column; display: flex; flex: 1 0; align-items: center;'
div.appendChild(x)
const lbl = document.createElement('label')
lbl.appendChild(document.createTextNode('Mixes'))
lbl.style = 'color: whitesmoke'
x.appendChild(lbl)
const cb = document.createElement('input')
x.appendChild(cb)
cb.setAttribute('type', 'checkbox')
cb.setAttribute('id', 'simplevarMixesCB')
}
{
const x = document.createElement('div')
x.style = 'flex-direction: column; display: flex; flex: 1 0; align-items: center; margin-left: .8em'
div.appendChild(x)
const lbl = document.createElement('label')
lbl.appendChild(document.createTextNode('Watch %'))
lbl.style = 'color: whitesmoke'
x.appendChild(lbl)
const txt = document.createElement('input')
x.appendChild(txt)
txt.setAttribute('type', 'number')
txt.setAttribute('min', '0')
txt.setAttribute('max', '100')
txt.setAttribute('id', 'simplevarWatchPercents')
}
const mixesCb = container.querySelector('#simplevarMixesCB')
mixesCb.checked = !!localStorage.getItem('simplevarMixesCB')
mixesCb.addEventListener('input', onCB)
onCB()
function onCB() {
localStorage.setItem('simplevarMixesCB', mixesCb.checked ? 1 : '')
if (mixesCb.checked) styleMixes.remove()
else document.head.appendChild(styleMixes)
}
const watchPercents = container.querySelector('#simplevarWatchPercents')
watchPercents.value = +(localStorage.getItem('simplevarWatchPercents') || 0)
watchPercents.addEventListener('input', onWatchPercentsChanged)
onWatchPercentsChanged()
function onWatchPercentsChanged() {
localStorage.setItem('simplevarWatchPercents', watchPercents.value)
const p = +watchPercents.value
if (p < used_styleWatchPercents) {
for (let i = p+1; i <= used_styleWatchPercents; i++) styleOn(styleWatchPercents[i-1])
}
else {
for (let i = used_styleWatchPercents + 1; i <= p; i++) styleOff(styleWatchPercents[i-1])
}
used_styleWatchPercents = p
}
}, 0);
// remove split scroll from related videos
function handleNode(n) {
if (n.tagName === 'YTD-WATCH-FLEXY') n.removeAttribute('split-scroll')
}
const observer = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === "childList") mutation.addedNodes.map(handleNode)
else if (mutation.type === "attributes") handleNode(mutation.target)
}
})
observer.observe(document.documentElement, { attributes: true, subtree: true });
})()