Instantly share code, notes, and snippets.
Last active
August 12, 2026 19:43
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save klydeinside/3e2babc1e2f21d458cb9b4214be13c2e to your computer and use it in GitHub Desktop.
Twitch userscripts - Live Follows Shortcut Button
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
| // ==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