Created
June 11, 2026 02:41
-
-
Save noizbuster/5788014650d180fa81691b496d42937f to your computer and use it in GitHub Desktop.
Notion Layout Shift Bug Fix
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 Notion Layout Left-Shift Bug Fix | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2026-06-09 | |
| // @description Comprehensive CSS reinforcement to fix Notion layout shifting left on PgUp/PgDn in modern Chrome. | |
| // @author NoizBuster | |
| // @match https://app.notion.com/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=notion.com | |
| // @grant GM_addStyle | |
| // @run-at document-start | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const DEBUG = false; | |
| function log(...args) { | |
| if (DEBUG) console.log('[Notion PgUp/PgDn Fix]', ...args); | |
| } | |
| function isPageKey(event) { | |
| return event.key === 'PageDown' || event.key === 'PageUp'; | |
| } | |
| function isEditableTarget(target) { | |
| if (!target || !(target instanceof Element)) return false; | |
| const tagName = target.tagName?.toLowerCase(); | |
| return ( | |
| target.isContentEditable || | |
| tagName === 'input' || | |
| tagName === 'textarea' || | |
| tagName === 'select' || | |
| target.closest('[contenteditable="true"]') || | |
| target.closest('[role="textbox"]') | |
| ); | |
| } | |
| function getAllScrollableElements() { | |
| const selectors = [ | |
| 'html', | |
| 'body', | |
| '#notion-app', | |
| '.notion-app-inner', | |
| '.notion-frame', | |
| '.notion-scroller', | |
| '.notion-scroller.vertical', | |
| '[data-testid="notion-app"]', | |
| '[data-block-id]', | |
| '[style*="overflow"]', | |
| '[style*="transform"]' | |
| ]; | |
| const set = new Set(); | |
| for (const selector of selectors) { | |
| document.querySelectorAll(selector).forEach((el) => set.add(el)); | |
| } | |
| set.add(document.documentElement); | |
| set.add(document.body); | |
| if (document.scrollingElement) { | |
| set.add(document.scrollingElement); | |
| } | |
| return [...set].filter(Boolean); | |
| } | |
| function neutralizeHorizontalScroll() { | |
| const elements = getAllScrollableElements(); | |
| for (const el of elements) { | |
| try { | |
| if ('scrollLeft' in el && el.scrollLeft !== 0) { | |
| log('reset scrollLeft', el, el.scrollLeft); | |
| el.scrollLeft = 0; | |
| } | |
| } catch (_) {} | |
| } | |
| try { | |
| window.scrollTo({ | |
| left: 0, | |
| top: window.scrollY, | |
| behavior: 'auto' | |
| }); | |
| } catch (_) { | |
| window.scrollTo(0, window.scrollY); | |
| } | |
| } | |
| function injectCss() { | |
| if (document.getElementById('notion-pgup-pgdn-horizontal-fix-style')) { | |
| return; | |
| } | |
| const style = document.createElement('style'); | |
| style.id = 'notion-pgup-pgdn-horizontal-fix-style'; | |
| style.textContent = ` | |
| html, | |
| body { | |
| max-width: 100vw !important; | |
| overflow-x: clip !important; | |
| } | |
| #notion-app, | |
| .notion-app-inner, | |
| .notion-frame { | |
| max-width: 100vw !important; | |
| overflow-x: clip !important; | |
| } | |
| .notion-scroller.vertical { | |
| overflow-x: hidden !important; | |
| overscroll-behavior-x: none !important; | |
| } | |
| body { | |
| position: relative !important; | |
| } | |
| `; | |
| const append = () => { | |
| if (document.head) { | |
| document.head.appendChild(style); | |
| } else { | |
| requestAnimationFrame(append); | |
| } | |
| }; | |
| append(); | |
| } | |
| function runCorrectionBurst() { | |
| neutralizeHorizontalScroll(); | |
| requestAnimationFrame(() => { | |
| neutralizeHorizontalScroll(); | |
| requestAnimationFrame(() => { | |
| neutralizeHorizontalScroll(); | |
| }); | |
| }); | |
| setTimeout(neutralizeHorizontalScroll, 0); | |
| setTimeout(neutralizeHorizontalScroll, 16); | |
| setTimeout(neutralizeHorizontalScroll, 50); | |
| setTimeout(neutralizeHorizontalScroll, 120); | |
| setTimeout(neutralizeHorizontalScroll, 250); | |
| } | |
| function onKeyDown(event) { | |
| if (!isPageKey(event)) return; | |
| // Ctrl / Alt / Meta / Shift 조합은 브라우저나 OS 단축키일 수 있으므로 건드리지 않음 | |
| if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) { | |
| return; | |
| } | |
| // Notion 에디터 내부에서 텍스트 편집 중이어도 PageUp/PageDown은 보통 페이지 이동 목적이므로 | |
| // 완전히 막지는 않고, 수평 깨짐만 보정한다. | |
| log('page key detected', event.key, 'editable:', isEditableTarget(event.target)); | |
| runCorrectionBurst(); | |
| } | |
| function onKeyUp(event) { | |
| if (!isPageKey(event)) return; | |
| runCorrectionBurst(); | |
| } | |
| function observeLayoutChanges() { | |
| const observer = new MutationObserver(() => { | |
| // Notion이 페이지 구조를 교체하는 경우 CSS가 빠지지 않도록 재삽입 | |
| injectCss(); | |
| }); | |
| const start = () => { | |
| if (!document.documentElement) { | |
| requestAnimationFrame(start); | |
| return; | |
| } | |
| observer.observe(document.documentElement, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| }; | |
| start(); | |
| } | |
| injectCss(); | |
| window.addEventListener('keydown', onKeyDown, true); | |
| window.addEventListener('keyup', onKeyUp, true); | |
| document.addEventListener('keydown', onKeyDown, true); | |
| document.addEventListener('keyup', onKeyUp, true); | |
| observeLayoutChanges(); | |
| // 초기 로딩 중 이미 수평 스크롤이 생기는 경우 방어 | |
| window.addEventListener('load', () => { | |
| injectCss(); | |
| runCorrectionBurst(); | |
| }); | |
| document.addEventListener('visibilitychange', () => { | |
| if (!document.hidden) { | |
| injectCss(); | |
| runCorrectionBurst(); | |
| } | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment