Skip to content

Instantly share code, notes, and snippets.

@klydeinside
Last active August 12, 2026 19:43
Show Gist options
  • Select an option

  • Save klydeinside/3e2babc1e2f21d458cb9b4214be13c2e to your computer and use it in GitHub Desktop.

Select an option

Save klydeinside/3e2babc1e2f21d458cb9b4214be13c2e to your computer and use it in GitHub Desktop.
Twitch userscripts - Live Follows Shortcut Button
// ==UserScript==
// @name Twitch – Live Follows Shortcut Button
// @namespace https://cody-tools.local
// @version 1.5
// @description Adds a one-click button right next to the side-nav collapse arrow (same icon/style as Twitch's own "Collapse Chat" button) that jumps straight to https://www.twitch.tv/directory/following/live using Twitch's own client-side router — so a stream you're watching drops into mini-player instead of the whole site reloading. The mini-player itself dims to 45% opacity so it doesn't block the grid behind it, and snaps to full opacity on hover.
// @match https://www.twitch.tv/*
// @grant none
// @updateURL https://gist.githubusercontent.com/klydeinside/3e2babc1e2f21d458cb9b4214be13c2e/raw/twitch-open-chat-button.user.js
// @downloadURL https://gist.githubusercontent.com/klydeinside/3e2babc1e2f21d458cb9b4214be13c2e/raw/twitch-open-chat-button.user.js
// ==/UserScript==
(function () {
'use strict';
const BTN_ID = 'lgGoLiveFollowsBtn';
const DEST = 'https://www.twitch.tv/directory/following/live';
// side-nav-arrow is present on every Twitch page, so this button shows
// up sitewide right next to it.
const SIDE_ARROW = '[data-a-target="side-nav-arrow"]';
// Twitch's own nav elements. Clicking these (they're real React-managed
// anchors) triggers Twitch's client-side router, which is what keeps a
// playing stream alive in the mini-player. A raw location.href navigation
// or a synthetic <a> we create ourselves both force a full page reload
// instead — verified against the live site before shipping this.
const FOLLOWING_LINK = '[data-a-target="following-link"]';
const LIVE_TAB = '[data-a-target="following-live-tab"]';
function goToLiveFollows() {
const followingLink = document.querySelector(FOLLOWING_LINK);
if (!followingLink) {
// Fallback: shouldn't happen (following-link is sitewide), but
// don't leave the button dead if Twitch changes this element.
window.location.href = DEST;
return;
}
followingLink.click();
const start = Date.now();
const poll = setInterval(() => {
const liveTab = document.querySelector(LIVE_TAB);
if (liveTab) {
clearInterval(poll);
liveTab.click();
} else if (Date.now() - start > 3000) {
clearInterval(poll); // give up quietly rather than hang forever
}
}, 50);
}
function insertButton() {
if (document.getElementById(BTN_ID)) return;
const sideArrow = document.querySelector(SIDE_ARROW);
if (!sideArrow) return;
const wrapper = sideArrow.parentElement;
const row = wrapper.parentElement;
if (!wrapper || !row) return;
const btn = document.createElement('button');
btn.id = BTN_ID;
// Reuse Twitch's own button classes so it inherits native styling
// (hover/focus states, sizing) instead of hand-rolled CSS.
btn.className = sideArrow.className;
btn.setAttribute('aria-label', 'Go to Live Follows');
btn.style.marginLeft = '4px';
btn.innerHTML = `<div class="ButtonIconFigure-sc-1emm8lf-0 jgYFuA"><div class="ScSvgWrapper-sc-wkgzod-0 cnGLHG tw-svg"><svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" role="presentation"><path d="M3 5h2v14H3V5Zm19.414 7-6.707-6.707-1.414 1.414L18.586 11H7v2h11.586l-4.293 4.293 1.414 1.414L22.414 12Z"></path></svg></div></div>`;
btn.addEventListener('click', goToLiveFollows);
const newWrapper = wrapper.cloneNode(false);
newWrapper.removeAttribute('id');
newWrapper.appendChild(btn);
row.insertBefore(newWrapper, wrapper.nextSibling);
}
// Twitch's floating mini-player (shown when you navigate away from a
// stream while it's playing) is a fixed-position box that sits over
// whatever's underneath — verified selector against the live site.
// Dim it at rest so the grid behind it stays readable, full opacity on
// hover so it's not washed out when you actually look at it.
const MINI_PLAYER_STYLE_ID = 'lgMiniPlayerDim';
function injectMiniPlayerStyle() {
if (document.getElementById(MINI_PLAYER_STYLE_ID)) return;
const style = document.createElement('style');
style.id = MINI_PLAYER_STYLE_ID;
style.textContent = `
.persistent-player {
opacity: 0.45;
transition: opacity 0.25s ease;
}
.persistent-player:hover {
opacity: 1;
}
`;
document.head.appendChild(style);
}
function tick() {
insertButton();
injectMiniPlayerStyle();
}
let tickScheduled = false;
function scheduleTick() {
if (tickScheduled) return;
tickScheduled = true;
requestAnimationFrame(() => {
tickScheduled = false;
tick();
});
}
// Twitch is an SPA — hook history navigation and watch for DOM changes
// so the button survives route changes.
const origPushState = history.pushState;
history.pushState = function (...args) {
origPushState.apply(this, args);
scheduleTick();
};
window.addEventListener('popstate', scheduleTick);
const observer = new MutationObserver(scheduleTick);
observer.observe(document.body, { childList: true, subtree: true });
tick();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment