// ==UserScript==
// @name Restore animated thumbnail previews - youtube.com
// @namespace Violentmonkey Scripts seekhare
// @match http*://www.youtube.com/*
// @run-at document-start
// @grant GM_addStyle
// @version 4.0
// @license MIT
// @author seekhare
// @description To restore animated thumbnail previews. Requires inline video previews to be disabled in your YouTube user settings (Go to https://www.youtube.com/account_playback and set "video previews" to disabled). Not Greasemonkey compatible. v4 Add new carousel fallback for Youtube's new homepage UI.
// ==/UserScript==
const logHeader = 'UserScript Restore YT Animated Thumbs:';
console.log(logHeader, "enabled.")
Object.defineProperties(Object.prototype,{isPreviewDisabled:{get:function(){return false}, set:function(){}}}); // original method
//2025-07-12 added animatedThumbnailEnabled & inlinePreviewEnabled
Object.defineProperties(Object.prototype,{animatedThumbnailEnabled:{get:function(){return true}, set:function(){}}});
Object.defineProperties(Object.prototype,{inlinePreviewEnabled:{get:function(){return false}, set:function(){}}});
//2025-07-12 Don't enable the below as seems to break things but I'm leaving here in case of future Youtube change, for reference if needed in future fixes.
//Object.defineProperties(Object.prototype,{isInlinePreviewEnabled:{get:function(){return true}, set:function(){return true}}});
//Object.defineProperties(Object.prototype,{isInlinePreviewDisabled:{get:function(){return true}, set:function(){return true}}});
//2025-07-28
//Object.defineProperties(Object.prototype,{ui.inlinePreviewIsActive:{get:function(){return false}, set:function(){}}});
//Object.defineProperties(Object.prototype,{ui.inlinePreviewIsEnabled:{get:function(){return false}, set:function(){}}});
//Object.defineProperties(Object.prototype,{inlinePreviewIsActive:{get:function(){return false}, set:function(){}}});
//Object.defineProperties(Object.prototype,{inlinePreviewIsEnabled:{get:function(){return false}, set:function(){}}});
fadeInCSS = `img.animatedThumbTarget { animation: fadeIn 0.5s; object-fit: cover;}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
`;
GM_addStyle(fadeInCSS);
const forceDisableNewHomepageMethod = false; // force disable the new homepage method.
const forceEnableNewHomepageMethod = false; // force disable takes priority over force enable.
const homeUrl = 'https://www.youtube.com/';
const ytImageBaseUrl = 'https://i.ytimg.com/vi/';
const ytImageNames = ['hq1.jpg', 'hq2.jpg', 'hq3.jpg']; // e.g. https://i.ytimg.com/vi/UujGYE5mOnI/0.jpg
const carouselDelay = 750; //milliseconds, how long to display each image.
function animatedThumbsEventEnter(event) {
//console.debug(logHeader, 'enter', event);
var target = event.target;
//console.debug(logHeader, 'target', target);
if (target.querySelector('badge-shape.badge-shape-wiz--thumbnail-live') != null) { // don't apply to video tiles that are live, can't do in observer as child element not present then.
target.removeEventListener('mouseenter', animatedThumbsEventEnter);
target.removeEventListener('mouseleave', animatedThumbsEventLeave);
return false
}
if (target.querySelector('ytm-shorts-lockup-view-model') != null) { // don't apply to shorts tiles, can't do in observer as child element not present then.
target.removeEventListener('mouseenter', animatedThumbsEventEnter);
target.removeEventListener('mouseleave', animatedThumbsEventLeave);
return false
}
var atag = target.querySelector('a');
//console.debug(logHeader, 'atag', atag);
if (atag.hasAttribute('videoId') === false) {
//extract videoId from href and store on an attribute
var videoId = atag.getAttribute('href').match(/watch\?v=([^&]*)/)[1]; //the href is like "/watch?v=IDabc123&t=123" so regex.
//console.debug(logHeader, 'videoId', videoId);
atag.setAttribute('videoId', videoId);
}
var animatedImgNode = document.createElement("img");
animatedImgNode.setAttribute('videoId', atag.getAttribute('videoId'));
animatedImgNode.setAttribute("carouselIndex", 0);
animatedImgNode.setAttribute("id", "thumbnail");
animatedImgNode.setAttribute("class", "style-scope ytd-moving-thumbnail-renderer fade-in animatedThumbTarget"); //animatedThumbTarget is custom class, others are Youtube
updateCarousel(animatedImgNode);
var overlaytag = target.querySelector('div.yt-thumbnail-view-model__image');
if (overlaytag == null) {
target.removeEventListener('mouseenter', animatedThumbsEventEnter);
target.removeEventListener('mouseleave', animatedThumbsEventLeave);
return false
}
overlaytag.appendChild(animatedImgNode);
animatedImgNode.timer = setInterval(updateCarousel, carouselDelay, animatedImgNode);
return true
}
function animatedThumbsEventLeave(event) {
//console.debug(logHeader, 'leave', event);
try {
var animatedImgNode = event.target.querySelector('img.animatedThumbTarget');
clearTimeout(animatedImgNode.timer);
animatedImgNode.remove();
} catch {}
return
}
function updateCarousel(animatedImgNode) {
var index = parseInt(animatedImgNode.getAttribute("carouselIndex"));
//console.debug(logHeader, 'index', index);
var imgURL = ytImageBaseUrl + animatedImgNode.getAttribute('videoId') + '/' + ytImageNames[index];
animatedImgNode.setAttribute("src", imgURL);
var nextIndex = (index+1) % ytImageNames.length;
animatedImgNode.setAttribute("carouselIndex", nextIndex);
}
function useNewSearchMethod() {
if (forceDisableNewHomepageMethod) {
return false
} else if (forceEnableNewHomepageMethod) {
return true
}
if (window.location.pathname === '/') {
console.debug(logHeader, 'Pathname check method');
if (document.head.innerHTML.indexOf('an_webp') != -1 || document.body.innerHTML.indexOf('an_webp') != -1) {
return false
}
else {
return true
}
} else {
// if not entered youtube via homepage then do a request here to determine if user's homepage is affected by YouTube's changes removing animated thumbs.
console.debug(logHeader, 'XMLHttpRequest check method');
const request = new XMLHttpRequest();
request.open("GET", homeUrl, false); // `false` makes the request synchronous
request.send(null);
if (request.status === 200) {
//console.debug('response', request.responseText);
var trimmedResponseIndex = request.responseText.indexOf('an_webp/');
if (trimmedResponseIndex != -1) {
return false
}
else {
return true
}
} else {
console.error(logHeader, 'Could not GET "'+homeUrl+'". Response Status = '+request.status, request.statusText);
return true
}
}
}
function runPageCheckForExistingElements() {
//Can run this just incase some elements were already created before observer set up.
var list = document.getElementsByTagName("ytd-rich-item-renderer");
for (var element of list) {
//console.debug(logHeader, element);
element.addEventListener('mouseenter', animatedThumbsEventEnter);
element.addEventListener('mouseleave', animatedThumbsEventLeave);
}
}
function setupMutationObserverSingle() {
if (useNewSearchMethod() === false) {
return console.log(logHeader, "Using old method only (preferred), disabling new method.")
}
console.log(logHeader, "Enabling new image method for homepage.")
const targetNode = document;
//console.debug('targetNodeInit',targetNode);
const config = {attributes: false, childList: true, subtree: true};
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
//console.debug(logHeader, "Mutation", mutation);
for (const element of mutation.addedNodes) {
if (element.nodeName === 'YTD-RICH-ITEM-RENDERER') {
//console.debug(logHeader, "Adding event listeners to element", element);
element.addEventListener('mouseenter', animatedThumbsEventEnter);
element.addEventListener('mouseleave', animatedThumbsEventLeave);
}
}
}
}
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
runPageCheckForExistingElements();
}
document.addEventListener("DOMContentLoaded", function(){
setupMutationObserverSingle()
});