Injects a custom speed control button into the YouTube player
// ==UserScript==
// @name YouTube Custom Speed Control Button
// @namespace http://tampermonkey.net/
// @version 2026-04-30
// @description Injects a custom speed control button into the YouTube player
// @author You
// @match *://*.youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const trustedPolicy =
window.trustedTypes && window.trustedTypes.createPolicy
? window.trustedTypes.createPolicy("youtube-speed-btn-policy", {
createHTML: (string) => string,
})
: null;
function injectSpeedButton() {
const buttonId = "custom-speed-btn";
// Prevent duplicate buttons
if (document.getElementById(buttonId)) return;
const rightControlsLeft = document.querySelector(
".ytp-right-controls .ytp-right-controls-left",
);
if (!rightControlsLeft) return;
const getCurrentVideo = () =>
document.querySelector("video.html5-main-video") ??
document.querySelector("video");
if (!getCurrentVideo()) return;
// Create the button
const btn = document.createElement("button");
btn.id = buttonId;
btn.className = "ytp-button"; // Uses YouTube's native button class
// // Add an SVG icon for the button
const svg1xSpeedString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.552 15.853q.714-.731.659-1.687t-.824-1.575a51 51 0 0 0-4.504-3.235A164 164 0 0 1 4.296 6.32a338 338 0 0 1 2.939 4.71 98 98 0 0 0 3.076 4.712q.55.815 1.538.83.99.014 1.703-.718M4.46 21q-.605 0-1.113-.267a2 2 0 0 1-.81-.802q-.77-1.35-1.153-2.826A12 12 0 0 1 1 14.08q.028-1.575.508-3.065.48-1.491 1.332-2.841l1.319 2.137q-.468.9-.715 1.87a8 8 0 0 0-.247 1.955q0 1.238.316 2.405t.948 2.208h15.133a9.4 9.4 0 0 0 .893-2.152q.316-1.139.316-2.348 0-3.74-2.568-6.37t-6.221-2.63q-1.017 0-1.991.253a9 9 0 0 0-1.854.703L6.08 4.856q1.318-.9 2.815-1.378A10.2 10.2 0 0 1 12.014 3q2.28 0 4.27.886 1.991.886 3.489 2.419a11.5 11.5 0 0 1 2.362 3.572Q23 11.916 23 14.25q0 1.518-.384 2.953a12 12 0 0 1-1.1 2.728 1.96 1.96 0 0 1-.823.802 2.4 2.4 0 0 1-1.099.267z" fill="#fff" /></svg>`;
const svg2xSpeedString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.416 21q-.606 0-1.115-.267a2 2 0 0 1-.812-.802 11.4 11.4 0 0 1-1.13-2.728 10.6 10.6 0 0 1-.357-2.953q0-2.644 1.142-4.992a11.1 11.1 0 0 1 3.208-3.952l.578 2.447a8.6 8.6 0 0 0-2.024 2.953 9.1 9.1 0 0 0-.702 3.544q0 1.181.303 2.334t.909 2.166h15.17q.579-1.04.895-2.18t.316-2.32q0-3.768-2.56-6.384t-6.25-2.616q-.275 0-.537.014-.261.015-.537.07L9.454 3.31q.634-.14 1.267-.225a10.53 10.53 0 0 1 5.561.802A11.1 11.1 0 0 1 19.78 6.29a11.4 11.4 0 0 1 2.354 3.572Q23 11.915 23 14.25q0 1.518-.385 2.94-.385 1.42-1.102 2.741a2 2 0 0 1-.812.802q-.51.267-1.115.267zm8.48-4.725a2.27 2.27 0 0 0 1.266-1.378q.33-.957-.22-1.772a191 191 0 0 0-3.235-4.584Q9.069 6.29 7.334 4.069a120 120 0 0 0 1.17 5.512 109 109 0 0 0 1.446 5.457q.274.927 1.17 1.28a2.2 2.2 0 0 0 1.776-.043" fill="#fff"/></svg>`;
const svg3xSpeedString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.414 21q-.606 0-1.115-.267a2 2 0 0 1-.812-.802 12.8 12.8 0 0 1-1.102-2.742A11.2 11.2 0 0 1 1 14.25q0-2.335.867-4.387A11.4 11.4 0 0 1 4.221 6.29a11.1 11.1 0 0 1 3.497-2.405 10.53 10.53 0 0 1 5.561-.802q.634.086 1.267.225l-1.46 2.025a4 4 0 0 0-.536-.07q-.263-.014-.537-.014-3.69 0-6.25 2.616t-2.56 6.384q0 1.18.316 2.32.317 1.14.895 2.18h15.17a8.3 8.3 0 0 0 .909-2.166 9.2 9.2 0 0 0 .303-2.334 9.1 9.1 0 0 0-.703-3.544 8.6 8.6 0 0 0-2.023-2.953l.578-2.447a11.1 11.1 0 0 1 3.208 3.952 11.3 11.3 0 0 1 1.142 4.992 10.6 10.6 0 0 1-.358 2.953 11.4 11.4 0 0 1-1.129 2.728 2 2 0 0 1-.812.802q-.51.267-1.115.267zm6.69-4.725a2.2 2.2 0 0 0 1.776.042q.896-.35 1.17-1.28a109 109 0 0 0 1.446-5.456q.647-2.727 1.17-5.512a167 167 0 0 0-3.373 4.472q-1.638 2.25-3.235 4.584-.55.816-.22 1.772a2.27 2.27 0 0 0 1.266 1.378" fill="#fff"/></svg>`;
const svg4xSpeedString = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 16.598q1.044-.014 1.539-.773l6.16-9.45-9.24 6.3q-.742.506-.783 1.547t.618 1.716 1.705.66M12 3q1.62 0 3.12.464t2.819 1.392l-2.09 1.35a8.2 8.2 0 0 0-3.85-.956q-3.657 0-6.228 2.63T3.2 14.25q0 1.181.316 2.334t.894 2.166h15.179a8.3 8.3 0 0 0 .92-2.222 9.8 9.8 0 0 0 .29-2.39q0-1.013-.234-1.97a8.3 8.3 0 0 0-.702-1.855l1.32-2.138q.826 1.322 1.306 2.813.48 1.49.51 3.093.027 1.603-.358 3.066a11.8 11.8 0 0 1-1.128 2.784q-.301.507-.825.788a2.3 2.3 0 0 1-1.1.281H4.41q-.578 0-1.1-.281a2.1 2.1 0 0 1-.825-.788A11.4 11.4 0 0 1 1 14.25q0-2.334.866-4.373a11.5 11.5 0 0 1 2.365-3.572q1.5-1.533 3.506-2.42A10.4 10.4 0 0 1 11.999 3" fill="#fff"/></svg>`;
const getSpeedConfig = (speed) => {
if (speed < 2.0) {
return { icon: svg1xSpeedString, nextSpeed: 2.0 };
}
if (speed < 3.0) {
return { icon: svg2xSpeedString, nextSpeed: 3.0 };
}
if (speed < 4.0) {
return { icon: svg3xSpeedString, nextSpeed: 4.0 };
}
return { icon: svg4xSpeedString, nextSpeed: 1.0 };
};
const updateButtonState = () => {
const currentVideo = getCurrentVideo();
if (!currentVideo) return;
const { icon, nextSpeed } = getSpeedConfig(currentVideo.playbackRate);
const tooltip = `Play at ${nextSpeed.toFixed(1)}x speed`;
btn.setAttribute("title", tooltip);
btn.setAttribute("data-tooltip-title", tooltip);
btn.setAttribute("data-title-no-tooltip", tooltip);
btn.ariaLabel = tooltip;
btn.innerHTML = trustedPolicy ? trustedPolicy.createHTML(icon) : icon;
};
updateButtonState();
// The core logic
btn.addEventListener("click", () => {
const currentVideo = getCurrentVideo();
if (currentVideo) {
const { nextSpeed } = getSpeedConfig(currentVideo.playbackRate);
console.log(
`Changing speed from ${currentVideo.playbackRate} to ${nextSpeed}`,
);
currentVideo.playbackRate = nextSpeed;
updateButtonState();
setTimeout(updateButtonState, 150);
}
});
const listenerController = new AbortController();
document.addEventListener("ratechange", updateButtonState, {
capture: true,
signal: listenerController.signal,
});
document.addEventListener("loadedmetadata", updateButtonState, {
capture: true,
signal: listenerController.signal,
});
const cleanupObserver = new MutationObserver(() => {
if (!document.body.contains(btn)) {
listenerController.abort();
cleanupObserver.disconnect();
}
});
cleanupObserver.observe(document.body, { childList: true, subtree: true });
// Insert the button at the beginning of the right controls
rightControlsLeft.append(btn);
}
// Run once on initial page load
injectSpeedButton();
// Run every time YouTube finishes an in-page navigation
window.addEventListener("yt-navigate-finish", () => {
// Add a slight delay to ensure the video player DOM has caught up with the data layer
setTimeout(injectSpeedButton, 500);
});
})();