Skip to content

Instantly share code, notes, and snippets.

@rbreaves
Last active April 14, 2025 22:15
Show Gist options
  • Save rbreaves/d195582a42838627dd94d9707df0c53a to your computer and use it in GitHub Desktop.
Save rbreaves/d195582a42838627dd94d9707df0c53a to your computer and use it in GitHub Desktop.
Tmux or i3 style Tile Zooming for Vivaldi Web Browser
// References
// https://forum.vivaldi.net/topic/10549/modding-vivaldi?page=1
// https://forum.vivaldi.net/topic/33122/custom-keyboard-shortcuts-mod
// vivaldi://experiments
// Enable "Allow for using CSS modifications".
// Restart vivaldi
// mod /Applications/Vivaldi.app/Contents/Frameworks/Vivaldi\ Framework.framework/Versions/[current version]/Resources/vivaldi/window.html
// insert inside body tag
// <script src="custom.js"></script>
// Debug via inspect here on Window.html
// vivaldi://inspect/#apps/
// List all tiles
// [...document.querySelectorAll('.webpageview')].filter(el => el.offsetParent !== null)
(function tileZoomHotkeyMod() {
const SHORTCUTS = {
"Ctrl+Shift+Z": () => {
const stack = document.querySelector('#webpage-stack > .tiled.visible');
if (!stack) return;
const allViews = [...stack.querySelectorAll('.webpageview.tiled.visible.pagefocusstop')];
const activeView = stack.querySelector('.webpageview.active.tiled.visible.pagefocusstop');
if (!activeView || allViews.length < 2) return;
const isZoomed = activeView.dataset.zoomed === 'true';
if (!isZoomed) {
const allTiles = [...stack.querySelectorAll('.tile')];
allTiles.forEach(tile => {
const containsActive = tile.contains(activeView);
tile.dataset.prevDisplay = tile.style.display || '';
tile.dataset.prevFlex = tile.style.flex || '';
tile.dataset.prevHeight = tile.style.height || '';
tile.dataset.prevGridTemplateRows = tile.style.gridTemplateRows || '';
tile.dataset.prevGridTemplateColumns = tile.style.gridTemplateColumns || '';
if (!containsActive) {
tile.style.display = 'none';
} else {
tile.style.display = 'block';
tile.style.flex = '1 1 100%';
tile.style.height = '100%';
tile.style.gridTemplateRows = 'unset';
tile.style.gridTemplateColumns = 'unset';
}
});
let node = activeView.parentElement;
while (node && node !== stack) {
if (node.classList.contains('tiled')) {
node.dataset.prevGridTemplateRows = node.style.gridTemplateRows || '';
node.dataset.prevGridTemplateColumns = node.style.gridTemplateColumns || '';
node.style.gridTemplateRows = 'unset';
node.style.gridTemplateColumns = 'unset';
}
node = node.parentElement;
}
stack.dataset.prevStyle = stack.getAttribute('style') || '';
stack.style.gridTemplateRows = 'unset';
stack.style.gridTemplateColumns = '100%';
activeView.dataset.zoomed = 'true';
} else {
const allTiles = [...stack.querySelectorAll('.tile')];
allTiles.forEach(tile => {
tile.style.display = tile.dataset.prevDisplay || '';
tile.style.flex = tile.dataset.prevFlex || '';
tile.style.height = tile.dataset.prevHeight || '';
tile.style.gridTemplateRows = tile.dataset.prevGridTemplateRows || '';
tile.style.gridTemplateColumns = tile.dataset.prevGridTemplateColumns || '';
delete tile.dataset.prevDisplay;
delete tile.dataset.prevFlex;
delete tile.dataset.prevHeight;
delete tile.dataset.prevGridTemplateRows;
delete tile.dataset.prevGridTemplateColumns;
});
let node = activeView.parentElement;
while (node && node !== stack) {
if (node.classList.contains('tiled')) {
node.style.gridTemplateRows = node.dataset.prevGridTemplateRows || '';
node.style.gridTemplateColumns = node.dataset.prevGridTemplateColumns || '';
delete node.dataset.prevGridTemplateRows;
delete node.dataset.prevGridTemplateColumns;
}
node = node.parentElement;
}
stack.setAttribute('style', stack.dataset.prevStyle || '');
delete stack.dataset.prevStyle;
activeView.dataset.zoomed = 'false';
}
},
"Ctrl+Shift+H": () => {
const activeView = document.querySelector('.webpageview.active.tiled.visible.pagefocusstop');
if (!activeView) return;
const tile = activeView.closest('.tile');
if (!tile) return;
const container = tile.parentElement?.classList.contains('tiled') ? tile.parentElement : null;
if (!container) return;
const isHidden = tile.dataset.hidden === 'true';
if (!isHidden) {
tile.dataset.prevDisplay = tile.style.display || '';
tile.dataset.prevFlex = tile.style.flex || '';
tile.style.display = 'none';
tile.dataset.hidden = 'true';
if (container.style.gridTemplateRows.includes(' ')) {
container.dataset.prevGridTemplateRows = container.style.gridTemplateRows;
container.style.gridTemplateRows = container.style.gridTemplateRows.split(' ').slice(1).join(' ');
}
if (container.style.gridTemplateColumns.includes(' ')) {
container.dataset.prevGridTemplateColumns = container.style.gridTemplateColumns;
container.style.gridTemplateColumns = container.style.gridTemplateColumns.split(' ').slice(1).join(' ');
}
const siblings = [...container.querySelectorAll(':scope > .tile')].filter(t => t !== tile && t.style.display !== 'none');
siblings.forEach(sib => {
sib.dataset.prevFlexSibling = sib.style.flex || '';
sib.style.flex = '1 1 auto';
});
} else {
tile.style.display = tile.dataset.prevDisplay || '';
tile.style.flex = tile.dataset.prevFlex || '';
delete tile.dataset.prevDisplay;
delete tile.dataset.prevFlex;
delete tile.dataset.hidden;
if (container.dataset.prevGridTemplateRows) {
container.style.gridTemplateRows = container.dataset.prevGridTemplateRows;
delete container.dataset.prevGridTemplateRows;
}
if (container.dataset.prevGridTemplateColumns) {
container.style.gridTemplateColumns = container.dataset.prevGridTemplateColumns;
delete container.dataset.prevGridTemplateColumns;
}
const siblings = [...container.querySelectorAll(':scope > .tile')].filter(t => t !== tile);
siblings.forEach(sib => {
if ('prevFlexSibling' in sib.dataset) {
sib.style.flex = sib.dataset.prevFlexSibling;
delete sib.dataset.prevFlexSibling;
}
});
}
},
"Ctrl+Shift+R": () => {
const tiles = document.querySelectorAll('.tile');
tiles.forEach(tile => {
tile.style.display = '';
tile.style.flex = '';
tile.style.height = '';
tile.style.gridTemplateRows = '';
tile.style.gridTemplateColumns = '';
delete tile.dataset.prevDisplay;
delete tile.dataset.prevFlex;
delete tile.dataset.prevHeight;
delete tile.dataset.prevGridTemplateRows;
delete tile.dataset.prevGridTemplateColumns;
delete tile.dataset.hidden;
});
const tiledContainers = document.querySelectorAll('.tiled');
tiledContainers.forEach(container => {
container.style.gridTemplateRows = '';
container.style.gridTemplateColumns = '';
delete container.dataset.prevGridTemplateRows;
delete container.dataset.prevGridTemplateColumns;
});
const allViews = document.querySelectorAll('.webpageview.tiled.visible.pagefocusstop');
allViews.forEach(view => {
delete view.dataset.zoomed;
});
const stack = document.querySelector('#webpage-stack > .tiled.visible');
if (stack?.dataset.prevStyle) {
stack.setAttribute('style', stack.dataset.prevStyle || '');
delete stack.dataset.prevStyle;
}
console.log("[TileZoomHotkey] Layout reset.");
}
};
function keyCombo(sourceWindow, combination, autoRepeat) {
const customShortcut = SHORTCUTS[combination];
if (sourceWindow !== vivaldiWindowId || autoRepeat) return;
if (customShortcut) customShortcut();
}
function initMod() {
if (document.querySelector("#browser") && vivaldi.tabsPrivate) {
vivaldi.tabsPrivate.onKeyboardShortcut.addListener(keyCombo);
console.log("[TileZoomHotkey] Ready — Ctrl+Shift+Z (zoom), Ctrl+Shift+H (hide), Ctrl+Shift+R (reset).");
} else {
setTimeout(initMod, 500);
}
}
initMod();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment