Adds simple buttons to control items per row on Youtube's home feed, works for shorts and news sections too. Buttons can be hidden if needed.
// ==UserScript==
// @name YouTube Grid Row Controller
// @namespace https://github.com/HageFX-78
// @version 1.0
// @description Adds simple buttons to control items per row on Youtube's home feed, works for shorts and news sections too. Buttons can be hidden if needed.
// @author HageFX78
// @license MIT
// @match *://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function () {
'use strict';
// Configurable options
const hideControls = GM_getValue('hideControls', false); // set true to hide UI controls, it will use the default values instead
const transparentButtons = GM_getValue('transparentButtons', false); // set true to make the buttons transparent and less intrusive, only applies if hideControls is false
const defaultSettingValue = {
// Default values mainly used when if you want to hide the buttons, change the values to your liking
content: 4,
news: 5,
shorts: 6,
};
let currentSettingValues = {
content: GM_getValue('itemPerRow', defaultSettingValue.content),
news: GM_getValue('newsPerRow', defaultSettingValue.news),
shorts: GM_getValue('shortsPerRow', defaultSettingValue.shorts),
};
// Styles
const style = (css) => {
const el = document.createElement('style');
el.textContent = css;
document.head.appendChild(el);
return el;
};
// Some of it maybe irrelevant after so long, will cleanup someday...
style(`
${hideControls ? '' : '#chips-content{width: 92% !important;}'}
.justify-left-custom { justify-content: left !important; }
.justify-center-custom { justify-content: center !important; }
ytd-rich-item-renderer[rendered-from-rich-grid][is-in-first-column] { margin-left: calc(var(--ytd-rich-grid-item-margin) / 2) !important; }
ytd-rich-item-renderer[hidden][is-responsive-grid], [is-slim-media]{ display: block !important; }
ytd-rich-item-renderer{ margin-bottom: var(--ytd-rich-grid-row-margin) !important; }
.button-container.ytd-rich-shelf-renderer { display: none !important; }
#dismissible.ytd-rich-shelf-renderer {
padding-bottom: 0 !important;
border-bottom: none !important;
}
#selected-chip-content{
width: 0% !important;
}
#spacer.ytd-shelf-renderer {
flex: 9 !important; /* Spacing gets weird in subscription feed page */
}
ytd-feed-filter-chip-bar-renderer[frosted-glass-mode=with-chipbar] #chips-wrapper.ytd-feed-filter-chip-bar-renderer {
flex-direction: row;
}
.itemPerRowControl {
display: flex;
justify-content: right;
align-items: center;
z-index: 2025;
flex: 1;
gap: 10px;
box-sizing: border-box;
user-select: none;
width: 8%;
}
.itemPerRowControl button {
border: none;
color: var(--yt-spec-text-primary);
background-color:${transparentButtons ? 'transparent' : 'var(--yt-spec-badge-chip-background)'};
font-size: 24px;
text-align: center;
display: inline-block;
height: 30px;
aspect-ratio: 1/1;
border-radius: 50%;
}
.itemPerRowControl button:hover {
background-color: var(--yt-spec-button-chip-background-hover);
cursor: pointer;
}
`);
const dynamicStyle = style('');
function updatePageLayout() {
dynamicStyle.textContent = `
ytd-rich-grid-renderer {
--ytd-rich-grid-items-per-row: ${hideControls ? defaultSettingValue.content : currentSettingValues.content} !important;
}
ytd-rich-shelf-renderer:not([is-shorts]) {
--ytd-rich-grid-items-per-row: ${hideControls ? defaultSettingValue.news : currentSettingValues.news} !important;
}
ytd-rich-shelf-renderer[is-shorts] {
--ytd-rich-grid-slim-items-per-row: ${hideControls ? defaultSettingValue.shorts : currentSettingValues.shorts} !important;
--ytd-rich-grid-items-per-row: ${hideControls ? defaultSettingValue.shorts : currentSettingValues.shorts} !important;
}
`;
}
function saveValues() {
GM_setValue('itemPerRow', currentSettingValues.content);
GM_setValue('newsPerRow', currentSettingValues.news);
GM_setValue('shortsPerRow', currentSettingValues.shorts);
}
function updateAndSave() {
updatePageLayout();
saveValues();
}
function isCreatorPage() {
return location.pathname.startsWith('/@');
}
function initGlobalWatcher() {
const targets = [
{
selector: '#chips-wrapper',
type: 'content',
place: (anchor, control) => anchor.appendChild(control),
},
{
selector: 'ytd-rich-section-renderer #menu-container',
type: (node) => (node.closest('ytd-rich-section-renderer')?.querySelector('[is-shorts]') ? 'shorts' : 'news'),
place: (anchor, control) => anchor.parentNode.insertBefore(control, anchor),
},
{
selector: 'ytd-shelf-renderer #title-container.style-scope.ytd-shelf-renderer',
type: 'content',
place: (anchor, control) => anchor.appendChild(control),
},
];
scanExistingAnchors(targets); // Some elements load before observer can be hooked, like the #chips
const observer = new MutationObserver((muts) => {
for (const m of muts) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
for (const t of targets) {
const anchor = node.matches(t.selector) ? node : node.querySelector?.(t.selector);
if (anchor) tryAttachControl(anchor, t);
}
}
}
});
observer.observe(document.documentElement, { subtree: true, childList: true });
}
function tryAttachControl(anchor, t) {
if (!anchor) return;
if (isCreatorPage()) return;
// Prevent duplicates
if (anchor.parentNode?.querySelector?.('.itemPerRowControl')) return;
const type = typeof t.type === 'function' ? t.type(anchor) : t.type;
const control = createControlDivRaw(type);
// CENTER for #chips-wrapper and the shelf title container
if (t.selector === '#chips-wrapper') {
control.classList.add('justify-left-custom');
} else if (t.selector.startsWith('ytd-shelf-renderer')) {
control.classList.add('justify-center-custom');
}
t.place(anchor, control);
}
function createControlDivRaw(type) {
const controlDiv = document.createElement('div');
controlDiv.classList.add('style-scope', 'ytd-rich-grid-renderer', 'itemPerRowControl');
['-', '+'].forEach((symbol) => {
const btn = document.createElement('button');
btn.textContent = symbol;
btn.addEventListener('click', () => {
if (symbol === '+') currentSettingValues[type]++;
else if (currentSettingValues[type] > 1) currentSettingValues[type]--;
updateAndSave();
});
controlDiv.appendChild(btn);
});
return controlDiv;
}
function scanExistingAnchors(targets) {
for (const t of targets) {
document.querySelectorAll(t.selector).forEach((anchor) => {
tryAttachControl(anchor, t);
});
}
}
function setupGMMenu() {
function rebuildButtonStyles(newVal) {
document.querySelectorAll('.itemPerRowControl button').forEach((btn) => {
btn.style.backgroundColor = newVal ? 'transparent' : 'var(--yt-spec-badge-chip-background)';
});
}
function applyHideControls(newVal) {
const controls = document.querySelectorAll('.itemPerRowControl');
controls.forEach((c) => {
c.style.display = newVal ? 'none' : 'flex';
});
// force layout update
updatePageLayout();
}
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand(`Reset Values`, () => {
GM_setValue('itemPerRow', defaultSettingValue.content);
GM_setValue('newsPerRow', defaultSettingValue.news);
GM_setValue('shortsPerRow', defaultSettingValue.shorts);
currentSettingValues = { ...defaultSettingValue };
updatePageLayout();
});
GM_registerMenuCommand(`Toggle hideControls [ ${hideControls} ]`, () => {
let newVal = !GM_getValue('hideControls', false);
GM_setValue('hideControls', newVal);
applyHideControls(newVal);
});
GM_registerMenuCommand(`Toggle transparentButtons [ ${transparentButtons} ]`, () => {
let newVal = !GM_getValue('transparentButtons', false);
GM_setValue('transparentButtons', newVal);
rebuildButtonStyles(newVal);
});
}
}
// ----------------------------------- Main Execution -----------------------------------
setupGMMenu();
updatePageLayout();
if (!hideControls) initGlobalWatcher();
})();