ContinuousPlay keeps YouTube playing, automatically.
YouTube interrupts long sessions with the "Video paused. Continue watching?" prompt. ContinuousPlay watches for it and clicks Continue the instant it appears - so your music, lectures, and background video never stall. Works on Chrome, Opera, and Firefox.
Available for Chrome, Opera, and Firefox (AMO) - store listings pending review.


Why you will want it
One job, done quietly in the background.
Never stalls
Auto-clicks the "Continue watching?" confirmation the moment it appears. Playback just keeps going.
Zero setup
No options, no account, no popups. Install it and forget it - it only runs on youtube.com.
Private by design
No data collected, no network calls, no tracking. It asks for zero permissions beyond YouTube itself.
How it works
A tiny content script, nothing more.
- It watches the YouTube page for the idle "Video paused. Continue watching?" dialog.
- The instant that dialog appears, it clicks the Continue button for you.
- It then re-arms for next time - so a single sitting can run for hours untouched.
Privacy
The short version: it collects nothing.
Support
Found a bug, want a feature, or just have feedback? Two ways to reach us.
Community forum
Browse known issues, ask a question, or see what others have reported. Public and free.
Open the forumSource code
The complete, unminified source - exactly what runs in the published package.
No build step, bundler, or minifier. Load the unpacked folder as-is, or download the exact reviewed package: ContinuousPlay-1.0.1.zip.
manifest.json
{
"manifest_version": 3,
"name": "ContinuousPlay",
"version": "1.0.1",
"description": "Auto-clicks YouTube's 'Continue watching?' prompt so playback never stalls - runs only on youtube.com, collects no data.",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"content_scripts": [
{
"matches": ["https://www.youtube.com/*"],
"js": ["content.js"],
"run_at": "document_idle",
"all_frames": false
}
],
"web_accessible_resources": [
{
"resources": ["worker.js"],
"matches": ["https://www.youtube.com/*"]
}
]
}
content.js
/*
* ContinuousPlay - keep YouTube playing
* -------------------------------------
* YouTube shows a "Video paused. Continue watching?" dialog after a long idle
* period. This content script auto-clicks its confirm button so playback never
* stalls, with active verification:
*
* 1. Checks once per second (setInterval) AND instantly on DOM mutation
* (MutationObserver), so a missed mutation can never wedge it.
* 2. After clicking, it CONFIRMS the button actually left the DOM and does not
* immediately come back; if it is still present it retries the click.
*
* Runs only on youtube.com. No network requests, no storage, no tracking - it
* does exactly one thing, locally, in your browser.
*/
(function () {
"use strict";
var POLL_MS = 1000; // check at least once per second
var COOLDOWN_MS = 1500; // do not re-click the same dialog while it animates out
var CONFIRM_INTERVAL_MS = 250; // re-check cadence after a click
var CONFIRM_CHECKS = 6; // ~1.5s of post-click verification
var CONFIRM_CLEAN_NEEDED = 2; // consecutive "gone" checks to call it confirmed
var CLICK_RETRIES = 3; // click attempts before giving up
var lastClickTs = 0;
function log(msg) {
try { console.debug("[ContinuousPlay]", msg); } catch (e) {}
}
/* ---------------- button detection ---------------- */
function findConfirmButton() {
var dialog = document.querySelector("yt-confirm-dialog-renderer");
if (dialog) {
var byId = dialog.querySelector("#confirm-button");
if (byId) {
var clickable = resolveClickable(byId);
if (isVisible(clickable)) return clickable;
}
}
var globalConfirm = document.querySelector(
"yt-button-renderer#confirm-button, #confirm-button"
);
if (globalConfirm) {
var c2 = resolveClickable(globalConfirm);
if (isVisible(c2)) return c2;
}
var candidates = document.querySelectorAll(
"yt-spec-button-shape-next, .ytSpecButtonShapeNextHost, " +
"tp-yt-paper-button, button"
);
for (var i = 0; i < candidates.length; i++) {
var el = candidates[i];
if (!isVisible(el)) continue;
var label = (
(el.getAttribute("aria-label") || "") + " " + (el.textContent || "")
)
.trim()
.toLowerCase();
if (/\b(yes|continue watching|continue)\b/.test(label)) {
if (el.closest("yt-confirm-dialog-renderer, tp-yt-paper-dialog, ytd-popup-container")) {
return resolveClickable(el);
}
}
}
return null;
}
function resolveClickable(wrapper) {
if (!wrapper) return null;
var inner = wrapper.querySelector(
"yt-spec-button-shape-next button, .ytSpecButtonShapeNextHost button, " +
"button, a#button, a"
);
return inner || wrapper;
}
function isVisible(el) {
if (!el) return false;
var rect = el.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return false;
var style = window.getComputedStyle(el);
if (style.display === "none" || style.visibility === "hidden") return false;
if (parseFloat(style.opacity || "1") === 0) return false;
return true;
}
/* present + visible -> the clickable, else null */
function buttonPresent() {
var b = findConfirmButton();
return b && isVisible(b) ? b : null;
}
/* ---------------- click + post-click verification ---------------- */
function doClick(attempt) {
var btn = buttonPresent();
if (!btn) return;
lastClickTs = Date.now();
try { btn.click(); } catch (e) { log("click threw: " + e.message); }
log("clicked confirm (attempt " + attempt + ")");
// Verify the button leaves the DOM and does not come straight back.
var checks = 0;
var clean = 0;
var iv = setInterval(function () {
checks++;
if (!buttonPresent()) {
clean++;
if (clean >= CONFIRM_CLEAN_NEEDED) {
clearInterval(iv);
log("confirmed gone");
}
return;
}
// still present this check
clean = 0;
if (checks >= CONFIRM_CHECKS) {
clearInterval(iv);
if (attempt < CLICK_RETRIES) {
log("button still present, retrying click");
doClick(attempt + 1);
} else {
log("button did not disappear after " + CLICK_RETRIES + " clicks");
}
}
}, CONFIRM_INTERVAL_MS);
}
/* ---------------- scheduling ---------------- */
function maybeClick() {
var btn = buttonPresent();
if (btn && Date.now() - lastClickTs > COOLDOWN_MS) doClick(1);
return !!btn;
}
// Instant reaction to DOM changes while the tab is visible. NOTE: we call
// maybeClick() directly here, NOT via requestAnimationFrame -- rAF is PAUSED
// for hidden/minimized tabs, so an rAF-scheduled scan would never run.
var observer = new MutationObserver(function () { maybeClick(); });
// The once-per-second heartbeat runs in a Web Worker. A minimized window /
// hidden tab THROTTLES main-thread setInterval (to ~1/min after 5 min) and
// pauses rAF, so the prompt would be missed while minimized. A Worker's timer
// is NOT throttled, so the click still fires within ~1s. The worker is an
// extension resource (web_accessible) so the page CSP can't block it; if the
// Worker can't start we fall back to a (throttled) main-thread interval.
function startTicker() {
try {
var url = (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.getURL)
? chrome.runtime.getURL("worker.js") : null;
if (url) {
var w = new Worker(url);
w.onmessage = function () { maybeClick(); };
log("worker ticker started");
return;
}
} catch (e) {
log("worker failed, using interval fallback: " + e.message);
}
window.setInterval(maybeClick, POLL_MS);
}
function start() {
if (!document.body) {
window.setTimeout(start, 50);
return;
}
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["aria-hidden", "style", "class", "hidden"]
});
startTicker();
log("armed");
maybeClick();
}
start();
})();
worker.js
/*
* ContinuousPlay timer worker.
*
* A Web Worker's timers are NOT subject to the main-thread background-tab
* throttling that slows setInterval to ~1/min (and pauses requestAnimationFrame)
* when the Opera window is minimized or the tab is hidden. So we keep the
* once-per-second heartbeat here and post it to the content script, which does
* the actual DOM check + click. This is what makes ContinuousPlay keep firing
* while minimized.
*/
setInterval(function () { postMessage(0); }, 1000);
