Skip to content

Instantly share code, notes, and snippets.

@ahtcx
Created February 25, 2026 11:24
Show Gist options
  • Select an option

  • Save ahtcx/03ba03e64a1b08ff8dd6f151ba1d6d2f to your computer and use it in GitHub Desktop.

Select an option

Save ahtcx/03ba03e64a1b08ff8dd6f151ba1d6d2f to your computer and use it in GitHub Desktop.
YouTube Jump Ahead Shortcut User Script
// ==UserScript==
// @name YouTube Jump Ahead Shortcut
// @namespace https://aht.cx/userscripts/youtube-jump-ahead-shortcut
// @version 1.0
// @description Keyboard shortcut for the "Jump ahead" button available to YouTube Premium users
// @match https://www.youtube.com/watch*
// @run-at document-end
// @grant none
// ==/UserScript==
(function () {
"use strict";
const TRIGGER_KEYS = ["s", "S"];
const TARGET_BUTTON_TEXT = "Jump ahead";
const WAIT_TIMEOUT_MS = 2000;
const POLL_INTERVAL_MS = 50;
const ARROW_RIGHT_EVENT = {
key: "ArrowRight",
code: "ArrowRight",
keyCode: 39,
which: 39,
bubbles: true,
cancelable: true,
};
const getPlayer = () => document.querySelector(".html5-video-player");
const isTypingTarget = (el) => !!el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable);
const normalizeText = (s) => (s || "").trim().replace(/\s+/g, " ").toLowerCase();
const isClickableVisible = (el) => {
if (!el) return false;
if (el.getAttribute("aria-disabled") === "true" || el.disabled) return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const dispatchRightArrow = (target) => {
target.dispatchEvent(new KeyboardEvent("keydown", ARROW_RIGHT_EVENT));
target.dispatchEvent(new KeyboardEvent("keyup", ARROW_RIGHT_EVENT));
};
const findButtonByText = (player, label) => {
const needle = normalizeText(label);
for (const el of player.querySelectorAll('button, [role="button"]')) {
if (normalizeText(el.textContent) !== needle) continue;
if (isClickableVisible(el)) return el;
}
for (const el of player.querySelectorAll('button, [role="button"]')) {
if (!normalizeText(el.textContent).includes(needle)) continue;
if (isClickableVisible(el)) return el;
}
return null;
};
const waitForButtonAndClick = (player, label, timeoutMs, pollMs) =>
new Promise((resolve) => {
const startedAt = Date.now();
let pollTimer = null;
let observer = null;
const cleanup = (result) => {
if (pollTimer) clearInterval(pollTimer);
if (observer) observer.disconnect();
resolve(result);
};
const attempt = () => {
const btn = findButtonByText(player, label);
if (btn) {
setTimeout(() => btn.click(), 0);
return cleanup(true);
}
if (Date.now() - startedAt >= timeoutMs) cleanup(false);
};
observer = new MutationObserver(() => requestAnimationFrame(attempt));
observer.observe(player, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
});
pollTimer = setInterval(attempt, pollMs);
attempt();
});
const doAction = async () => {
const player = getPlayer();
if (!player) return;
dispatchRightArrow(document);
dispatchRightArrow(player);
await waitForButtonAndClick(player, TARGET_BUTTON_TEXT, WAIT_TIMEOUT_MS, POLL_INTERVAL_MS);
};
document.addEventListener(
"keydown",
(event) => {
if (isTypingTarget(event.target)) return;
if (!TRIGGER_KEYS.includes(event.key)) return;
event.preventDefault();
event.stopPropagation();
void doAction();
},
true,
);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment