Download the comments of any YouTube video as a spreadsheet: author, comment, likes, number of replies, when it was posted, pinned and hearted comments, and a link to each one. No API key, no sign-up, no extension.
Open the video (no need to sign in), choose Top or Newest above the comments if you want, then run the script.
// Responsive Muse · YouTube Comments Exporter · v1.0 · 2026-10-23
// Saves the comments of any YouTube video to CSV or TXT: author, text, likes, number of replies,
// when it was posted, pinned / hearted by the creator and a link to each comment. Uses the
// order you picked under the video (Top or Newest). YouTube loads more comments while the
// script scrolls. It makes no network requests of its own and never reads cookies,
// passwords or tokens.
(async () => {
const TOOL = 'Export YouTube comments';
const FORMAT = 'csv'; // 'csv' (Excel, Sheets) or 'txt' (author and comment, one block each)
const LIMIT = 1000; // stop after this many comments (0 = all; big videos have millions)
const video = new URLSearchParams(location.search).get('v') || (location.pathname.match(/^\/shorts\/([\w-]{11})/) || [])[1];
if (location.hostname !== 'www.youtube.com' || !video) {
alert('Open the YouTube video first, for example:\nhttps://www.youtube.com/watch?v=jNQXAC9IVRw');
return;
}
// A small box on the page shows progress and asks before changing anything,
// so you never need to read or edit the console.
const many = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`; // 1 video, 2 videos
const panel = (() => {
const box = document.createElement('div');
box.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
const head = document.createElement('strong');
head.textContent = TOOL;
const msg = document.createElement('p');
msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
const row = document.createElement('div');
row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
box.append(head, msg, row);
document.body.append(box);
const button = (label, primary, onClick) => {
const b = document.createElement('button');
b.textContent = label;
b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
(primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
b.onclick = onClick;
row.append(b);
};
return {
text(t) { msg.textContent = t; },
ask(t, yes) {
msg.textContent = t;
row.replaceChildren();
return new Promise(r => {
button('Cancel', false, () => { row.replaceChildren(); r(false); });
button(yes, true, () => { row.replaceChildren(); r(true); });
});
},
stoppable(onStop) { row.replaceChildren(); button('Stop', false, onStop); },
actions(t, list) {
msg.textContent = t;
row.replaceChildren();
list.forEach(([label, primary, run]) => button(label, primary, run));
},
finish(t) { this.actions(t, [['Close', true, () => box.remove()]]); },
close() { box.remove(); },
};
})();
// Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
const deliver = (summary, body, file) => {
const save = () => {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
a.download = file;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
panel.text(`${summary}\nSaved as ${file} (check your Downloads).`);
};
const copy = () => navigator.clipboard.writeText(body)
.then(() => panel.text(`${summary}\nCopied. Paste it into a spreadsheet or a note.`))
.catch(() => panel.text(`${summary}\nThe browser blocked the clipboard: use Download instead.`));
panel.actions(summary, [['Close', false, () => panel.close()], ['Copy', false, copy], ['Download', true, save]]);
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
const comments = new Map();
const clean = s => (s || '').replace(/\s+/g, ' ').trim();
// Read the comments loaded so far (top-level threads; replies are counted, not exported)
const collect = () => {
for (const thread of document.querySelectorAll('ytd-comment-thread-renderer')) {
const c = thread.querySelector('#comment, ytd-comment-view-model, ytd-comment-renderer');
if (!c) continue;
const when = c.querySelector('#published-time-text a') || c.querySelector('#published-time-text');
const id = ((when?.getAttribute('href') || '').match(/[?&]lc=([\w.-]+)/) || [])[1]
|| clean(c.querySelector('#author-text')?.textContent) + '|' + clean(c.querySelector('#content-text')?.textContent).slice(0, 80);
if (comments.has(id)) continue;
const replies = thread.querySelector('#more-replies button, #more-replies')?.textContent || '';
comments.set(id, {
n: comments.size + 1,
author: clean(c.querySelector('#author-text')?.textContent),
text: (c.querySelector('#content-text')?.textContent || '').trim(),
likes: clean(c.querySelector('#vote-count-middle')?.textContent),
replies: ((clean(replies).match(/[\d.,]+\s*[KkMm]?/) || [''])[0]).trim(),
posted: clean(when?.textContent),
pinned: c.querySelector('ytd-pinned-comment-badge-renderer, #pinned-comment-badge > *') ? 'yes' : '',
hearted: c.querySelector('#creator-heart #hearted, ytd-creator-heart-renderer #hearted, #creator-heart-button [aria-pressed="true"]') ? 'yes' : '',
url: id.includes('|') ? '' : `https://www.youtube.com/watch?v=${video}&lc=${id}`,
});
if (LIMIT && comments.size >= LIMIT) return;
}
};
// 1. Scroll down in short steps (YouTube draws comments as they come into view), collecting as
// we go, until the limit or nothing new appears for about 20 seconds (if YouTube stalls,
// scrolling up and down again makes it resume)
let stop = false;
panel.stoppable(() => { stop = true; });
document.querySelector('video')?.pause(); // otherwise autoplay can jump to the next video mid-export
window.scrollTo(0, 600); // the comments only start loading once they come into view
await sleep(2000);
let idle = 0;
while (idle < 32 && !stop && !(LIMIT && comments.size >= LIMIT)) {
const before = comments.size;
if (idle && idle % 8 === 0) {
window.scrollBy(0, -2500);
await sleep(800);
}
window.scrollBy(0, 1500);
await sleep(300);
const more = document.querySelector('ytd-comments ytd-continuation-item-renderer');
if (more && more.getBoundingClientRect().top < innerHeight * 2) more.scrollIntoView({ block: 'end' });
await sleep(300);
collect();
idle = comments.size === before ? idle + 1 : 0;
panel.text(`Reading comments… ${comments.size}${LIMIT ? ' of up to ' + LIMIT : ''}`);
}
const list = [...comments.values()];
if (!list.length) {
panel.finish('No comments found. Are comments turned off for this video? If not, YouTube may have changed its layout: please report it on responsive-muse.com.');
return 0;
}
// 2. Build the file and offer it
const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
const body = FORMAT === 'txt'
? list.map(c => `${c.author} · ${c.posted} · ${c.likes || 0} likes\n${c.text}`).join('\n\n')
: ['#,Author,Comment,Likes,Replies,Posted,Pinned,Hearted by creator,URL', ...list.map(c =>
[c.n, c.author, c.text, c.likes, c.replies, c.posted, c.pinned, c.hearted, c.url].map(cell).join(','))].join('\n');
const title = clean(document.querySelector('h1.ytd-watch-metadata, #title h1')?.textContent) || video;
const name = ('comments-' + title).slice(0, 80).replace(/[\\/:*?"<>|]+/g, '_');
deliver(`${stop ? 'Stopped early. ' : ''}Found ${many(list.length, 'comment')}${LIMIT && list.length >= LIMIT ? ' (limit reached)' : ''}.`, body, `${name}.${FORMAT}`);
return list.length;
})();
Bookmarklet: drag the middle button to your bookmarks bar, open www.youtube.com and click the bookmark. It runs the same script with the options above.
- Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is www.youtube.com itself loading your list.
- Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
- Only runs on www.youtube.com. Anywhere else it stops before doing anything.
- Short and readable. 146 commented lines, not minified or obfuscated.
- Shows its progress on the page. A small box tells you what it's doing and offers Download or Copy at the end.
- Same code for everyone. SHA-256 of the default version:
2896e588d237e0db…
Verify it yourself in 30 seconds
- Read it: every line is above, with comments.
- Search the code for
fetch,XMLHttpRequest,cookie,localStorageoreval. None of them appear. - Open the Network tab before running it: you'll only see requests to www.youtube.com.
How to use it
- Open the video on youtube.com. You don’t need to be signed in. To get the newest comments instead of the top ones, pick Newest first in the Sort by menu above the comments.
- Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type
allow pastingand press Enter. - Copy the script, paste it in the console and press Enter. It pauses the video, scrolls through the comments and then offers the file.
What you get
One row per comment with its author’s handle, the full text, likes and replies as YouTube shows them (4.8M, 986), how long ago it was posted, whether it is pinned or hearted by the creator, and a direct link that opens the video at that comment. Useful for research, giveaways, community management or simply keeping the discussion of your own videos.
Why not an online comment exporter?
Most of them need a YouTube API key or ask you to log in, and many cap the free version at a few hundred comments. This script reads the comments already loaded in your own browser, so there are no quotas and nothing to connect.
FAQ
Does it need an API key or my Google account?
No. Comment exporters usually need a YouTube API key or ask you to connect your account. This script reads the comments YouTube already shows you, on any video, signed in or not.
How many comments can it export?
As many as YouTube loads, about 500 a minute. Popular videos have millions, so it stops at 1,000 by default: raise the limit or set 0 for all.
Are replies included?
Each comment comes with its number of replies. The replies themselves aren't exported in this version.
Can I export the newest comments instead of the top ones?
Yes. Choose "Newest first" in the Sort by menu above the comments before running the script; it keeps the order you see.
