Last active
October 14, 2025 13:20
-
-
Save ajohnclark/8015c38015f66c8025b965e0c233cb53 to your computer and use it in GitHub Desktop.
Unfollow all on X.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Unfollow all on x.com (working as of 10/2025). Run in dev console on /following page. May break / need to run a few times, janky. | |
| (async function unfollowAll({ | |
| clickDelay = 700, // ms between individual clicks | |
| scrollDelay = 1200, // ms to wait after scrolling to load items | |
| max = 0 // 0 = unlimited, set >0 to stop after that many unfollows | |
| } = {}) { | |
| const sleep = ms => new Promise(res => setTimeout(res, ms)); | |
| // selector targets buttons that look like the "Following" buttons in the new HTML | |
| const selector = 'button[data-testid$="-unfollow"], button[aria-label^="Following"]'; | |
| // return only visible buttons that we haven't already marked/clicked | |
| function getVisibleButtons() { | |
| return Array.from(document.querySelectorAll(selector)) | |
| .filter(b => b.offsetParent !== null && !b.dataset.__unfollow_script_clicked); | |
| } | |
| // if an "Are you sure?" confirmation pops up, try to click an Unfollow/Confirm button | |
| function clickConfirmDialog() { | |
| // look for common labels in buttons inside dialogs | |
| const candidates = Array.from(document.querySelectorAll('div[role="dialog"] button, button')); | |
| const confirm = candidates.find(btn => /\bunfollow\b/i.test(btn.textContent)); | |
| if (confirm) { | |
| try { confirm.click(); return true; } catch(e) {} | |
| } | |
| return false; | |
| } | |
| let clicked = 0; | |
| let consecutiveScrollsWithoutNew = 0; | |
| const MAX_SCROLL_ATTEMPTS = 6; // how many scroll attempts when no new buttons appear | |
| console.log('Unfollow script started (clickDelay=%sms, scrollDelay=%sms, max=%s)', clickDelay, scrollDelay, max); | |
| while (true) { | |
| let buttons = getVisibleButtons(); | |
| if (buttons.length === 0) { | |
| // no visible buttons — try to load more by scrolling | |
| window.scrollBy(0, window.innerHeight); | |
| await sleep(scrollDelay); | |
| const buttonsAfter = getVisibleButtons(); | |
| if (buttonsAfter.length === 0) { | |
| consecutiveScrollsWithoutNew++; | |
| } else { | |
| consecutiveScrollsWithoutNew = 0; | |
| } | |
| // if we've tried scrolling several times and still nothing, assume we're done | |
| if (consecutiveScrollsWithoutNew >= MAX_SCROLL_ATTEMPTS) { | |
| console.log('No more unfollow buttons found after scrolling. Exiting.'); | |
| break; | |
| } | |
| continue; | |
| } | |
| // click through the visible buttons (one-by-one) | |
| for (const btn of buttons) { | |
| // mark so we don't re-click it if DOM doesn't change immediately | |
| btn.dataset.__unfollow_script_clicked = '1'; | |
| try { | |
| btn.click(); | |
| clicked++; | |
| // if a confirmation appears, try to accept it | |
| await sleep(250); | |
| clickConfirmDialog(); | |
| } catch (err) { | |
| console.warn('click failed', err); | |
| } | |
| console.log('Clicked unfollow #' + clicked, btn); | |
| // obey max | |
| if (max > 0 && clicked >= max) { | |
| console.log('Reached max limit (' + max + '). Stopping.'); | |
| return; | |
| } | |
| await sleep(clickDelay); | |
| } | |
| } | |
| console.log('Finished. Total unfollow clicks: ' + clicked); | |
| })(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment