Skip to content

Instantly share code, notes, and snippets.

@Astropulse
Created December 15, 2025 18:55
Show Gist options
  • Select an option

  • Save Astropulse/30a09ae130f9992bfe7b80310132845c to your computer and use it in GitHub Desktop.

Select an option

Save Astropulse/30a09ae130f9992bfe7b80310132845c to your computer and use it in GitHub Desktop.
Violentmonkey/greasemonkey extension to force 'following' page default on X
// ==UserScript==
// @name X Home Default to Following
// @namespace vm.x.following.default
// @version 1.1.0
// @description On X home, automatically switch to the "Following" tab instead of "For you".
// @match https://x.com/home
// @match https://x.com/home*
// @match https://twitter.com/home
// @match https://twitter.com/home*
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
"use strict";
const TARGET_TAB_TEXT = "following";
const AVOID_TAB_TEXT = "for you";
let lastActionAt = 0;
let pending = false;
function nowMs() {
return Date.now();
}
function normalizeText(s) {
return (s || "")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
function isElementVisible(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
if (!style) return false;
if (style.display === "none" || style.visibility === "hidden") return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function getHomePrimaryColumn() {
// X uses a primary column in the middle. We keep this loose on purpose.
// Try to anchor near the tabs area using role="tablist".
const tablist = document.querySelector('[role="tablist"]');
if (tablist) return tablist;
return null;
}
function getTabs() {
const root = getHomePrimaryColumn() || document;
const tabs = Array.from(root.querySelectorAll('[role="tab"]'));
// Sometimes tabs are anchors without role=tab but with role=tab on child.
// Keep only unique elements and those with readable text.
return tabs
.filter((t) => normalizeText(t.textContent).length > 0)
.filter((t) => isElementVisible(t));
}
function findTabByText(wantedLower) {
const tabs = getTabs();
for (const tab of tabs) {
const txt = normalizeText(tab.textContent);
if (txt === wantedLower) return tab;
}
// Fallback: partial match
for (const tab of tabs) {
const txt = normalizeText(tab.textContent);
if (txt.includes(wantedLower)) return tab;
}
return null;
}
function isSelectedTab(tab) {
if (!tab) return false;
const ariaSelected = tab.getAttribute("aria-selected");
if (ariaSelected === "true") return true;
// Some builds use data attributes or different patterns.
// If this tab contains an element that looks like "selected", keep it conservative.
const className = (tab.className || "").toString().toLowerCase();
if (className.includes("selected") || className.includes("active")) return true;
return false;
}
function onHomeRoute() {
const p = location.pathname || "";
return p === "/home";
}
function trySwitchToFollowing(reason) {
if (!onHomeRoute()) return;
const t = nowMs();
if (t - lastActionAt < 1000) return; // throttle
const followingTab = findTabByText(TARGET_TAB_TEXT);
const forYouTab = findTabByText(AVOID_TAB_TEXT);
// If we cannot find the tabs yet, bail and let observers retry.
if (!followingTab) return;
// If Following is already selected, nothing to do.
if (isSelectedTab(followingTab)) return;
// If For you is selected, click Following.
// If we cannot detect selection, still click Following once.
const shouldClick =
(forYouTab && isSelectedTab(forYouTab)) || !isSelectedTab(followingTab);
if (!shouldClick) return;
lastActionAt = t;
// Click in a way that works with React event handlers.
followingTab.dispatchEvent(
new MouseEvent("click", { bubbles: true, cancelable: true, view: window })
);
// Some UI states need focus first.
try {
followingTab.focus();
} catch (e) {}
// If the first click fails due to overlay timing, schedule one retry.
setTimeout(() => {
if (!isSelectedTab(findTabByText(TARGET_TAB_TEXT))) {
const retryTab = findTabByText(TARGET_TAB_TEXT);
if (retryTab) {
retryTab.dispatchEvent(
new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window,
})
);
}
}
}, 700);
}
function scheduleTry(reason) {
if (pending) return;
pending = true;
setTimeout(() => {
pending = false;
trySwitchToFollowing(reason);
}, 50);
}
function hookHistory() {
const origPushState = history.pushState;
const origReplaceState = history.replaceState;
function fire() {
scheduleTry("route");
}
history.pushState = function () {
const r = origPushState.apply(this, arguments);
fire();
return r;
};
history.replaceState = function () {
const r = origReplaceState.apply(this, arguments);
fire();
return r;
};
window.addEventListener("popstate", fire, true);
}
function observeDom() {
const obs = new MutationObserver(() => {
scheduleTry("dom");
});
obs.observe(document.documentElement, {
childList: true,
subtree: true,
});
}
// Init
hookHistory();
observeDom();
// Initial attempt
scheduleTry("init");
// Also retry a few times early to catch slow loads.
let tries = 0;
const early = setInterval(() => {
tries += 1;
scheduleTry("early");
if (tries >= 12) clearInterval(early);
}, 500);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment