Last active
June 10, 2026 16:51
-
-
Save mikestecker/1a541d69891cb8cfd4484e56a13b6176 to your computer and use it in GitHub Desktop.
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
| window.overflowDebug = function overflowDebug() { | |
| const STYLE_ID = 'overflow-debug-styles' | |
| const MARKER = 'data-overflow-debug' | |
| const CLIP_VALUES = ['hidden', 'clip', 'auto', 'scroll'] | |
| const originals = new Map() | |
| const removeHighlights = () => { | |
| document.querySelectorAll(`[${MARKER}]`).forEach((el) => { | |
| const prev = originals.get(el) | |
| if (prev) { | |
| el.style.outline = prev.outline | |
| el.style.outlineOffset = prev.outlineOffset | |
| el.style.boxShadow = prev.boxShadow | |
| el.style.backgroundColor = prev.backgroundColor | |
| el.style.position = prev.position | |
| el.style.zIndex = prev.zIndex | |
| } else { | |
| el.style.removeProperty('outline') | |
| el.style.removeProperty('outline-offset') | |
| el.style.removeProperty('box-shadow') | |
| el.style.removeProperty('background-color') | |
| el.style.removeProperty('position') | |
| el.style.removeProperty('z-index') | |
| } | |
| el.removeAttribute(MARKER) | |
| }) | |
| originals.clear() | |
| document.getElementById(STYLE_ID)?.remove() | |
| document.getElementById('overflow-debug-label')?.remove() | |
| } | |
| removeHighlights() | |
| const style = document.createElement('style') | |
| style.id = STYLE_ID | |
| style.textContent = ` | |
| @keyframes overflow-debug-pulse { | |
| 0%, 100% { box-shadow: 0 0 0 3px #ff2d55, 0 0 20px #ff2d55 !important; } | |
| 50% { box-shadow: 0 0 0 5px #ff0000, 0 0 30px #ff0000 !important; } | |
| } | |
| [${MARKER}] { | |
| outline: 3px solid #ff2d55 !important; | |
| outline-offset: 0 !important; | |
| background-color: rgba(255, 45, 85, 0.18) !important; | |
| animation: overflow-debug-pulse 1s ease-in-out infinite !important; | |
| position: relative !important; | |
| z-index: 2147483647 !important; | |
| } | |
| #overflow-debug-label { | |
| position: fixed; | |
| top: 8px; | |
| left: 8px; | |
| z-index: 2147483647; | |
| background: #ff2d55; | |
| color: #fff; | |
| font: 12px/1.4 monospace; | |
| padding: 6px 8px; | |
| border-radius: 4px; | |
| pointer-events: none; | |
| white-space: pre-wrap; | |
| max-width: calc(100vw - 16px); | |
| } | |
| ` | |
| document.head.appendChild(style) | |
| const getSelector = (el) => { | |
| if (!(el instanceof Element)) return String(el) | |
| if (el.id) return `#${el.id}` | |
| const parts = [] | |
| let node = el | |
| while (node && node.nodeType === 1 && parts.length < 6) { | |
| let part = node.tagName.toLowerCase() | |
| if (node.classList.length) { | |
| part += '.' + [...node.classList].slice(0, 2).join('.') | |
| } | |
| const parent = node.parentElement | |
| if (parent) { | |
| const siblings = [...parent.children].filter((c) => c.tagName === node.tagName) | |
| if (siblings.length > 1) { | |
| part += `:nth-of-type(${siblings.indexOf(node) + 1})` | |
| } | |
| } | |
| parts.unshift(part) | |
| node = node.parentElement | |
| } | |
| return parts.join(' > ') | |
| } | |
| const clipsHorizontalOverflow = (el) => { | |
| if (!(el instanceof Element)) return false | |
| const styles = getComputedStyle(el) | |
| if (CLIP_VALUES.includes(styles.overflowX)) return true | |
| if (CLIP_VALUES.includes(styles.overflow)) return true | |
| if (el.shadowRoot) { | |
| return [...el.shadowRoot.querySelectorAll('*')].some((child) => { | |
| if (!(child instanceof Element)) return false | |
| const childStyles = getComputedStyle(child) | |
| return ( | |
| CLIP_VALUES.includes(childStyles.overflowX) || | |
| CLIP_VALUES.includes(childStyles.overflow) | |
| ) | |
| }) | |
| } | |
| return false | |
| } | |
| const isClippedByAncestor = (el) => { | |
| let node = el.parentElement | |
| while (node && node !== document.documentElement) { | |
| if (clipsHorizontalOverflow(node)) return true | |
| node = node.parentElement | |
| } | |
| return false | |
| } | |
| const isWiderThanViewport = (el, rect, viewportWidth) => { | |
| return ( | |
| rect.width > viewportWidth + 1 || | |
| el.scrollWidth > viewportWidth + 1 || | |
| rect.right > viewportWidth + 1 || | |
| rect.left < -1 | |
| ) | |
| } | |
| const keepLeafOffenders = (offenders) => { | |
| return offenders.filter((item) => { | |
| return !offenders.some((other) => other.el !== item.el && item.el.contains(other.el)) | |
| }) | |
| } | |
| const highlightElement = (el) => { | |
| originals.set(el, { | |
| outline: el.style.outline, | |
| outlineOffset: el.style.outlineOffset, | |
| boxShadow: el.style.boxShadow, | |
| backgroundColor: el.style.backgroundColor, | |
| position: el.style.position, | |
| zIndex: el.style.zIndex | |
| }) | |
| el.setAttribute(MARKER, 'true') | |
| el.style.setProperty('outline', '3px solid #ff2d55', 'important') | |
| el.style.setProperty('outline-offset', '0', 'important') | |
| el.style.setProperty('box-shadow', '0 0 0 3px #ff2d55, 0 0 20px #ff2d55', 'important') | |
| el.style.setProperty('background-color', 'rgba(255, 45, 85, 0.18)', 'important') | |
| el.style.setProperty('position', 'relative', 'important') | |
| el.style.setProperty('z-index', '2147483647', 'important') | |
| } | |
| const viewportWidth = document.documentElement.clientWidth | |
| const scrollWidth = document.documentElement.scrollWidth | |
| const pageOverflow = Math.max(0, scrollWidth - viewportWidth) | |
| console.group('%cOverflow investigation', 'font-weight:bold;font-size:14px') | |
| console.log('Viewport width:', viewportWidth) | |
| console.log('Document scroll width:', scrollWidth) | |
| console.log('Page overflow:', pageOverflow > 1 ? `${pageOverflow}px` : 'none') | |
| console.groupEnd() | |
| const rawMatches = [] | |
| document.querySelectorAll('body *').forEach((el) => { | |
| const rect = el.getBoundingClientRect() | |
| const styles = getComputedStyle(el) | |
| if (styles.display === 'none' || styles.visibility === 'hidden') return | |
| if (rect.width <= 0 || rect.height <= 0) return | |
| if (!isWiderThanViewport(el, rect, viewportWidth)) return | |
| rawMatches.push({ | |
| el, | |
| selector: getSelector(el), | |
| clipped: isClippedByAncestor(el), | |
| width: Math.round(rect.width), | |
| height: Math.round(rect.height), | |
| scrollWidth: el.scrollWidth, | |
| overflowRight: Math.max(0, rect.right - viewportWidth), | |
| overflowLeft: Math.max(0, -rect.left), | |
| styles: { | |
| width: styles.width, | |
| minWidth: styles.minWidth, | |
| maxWidth: styles.maxWidth, | |
| marginLeft: styles.marginLeft, | |
| marginRight: styles.marginRight, | |
| overflowX: styles.overflowX, | |
| position: styles.position | |
| } | |
| }) | |
| }) | |
| const offenders = keepLeafOffenders(rawMatches.filter((item) => !item.clipped)) | |
| offenders.sort((a, b) => { | |
| const aScore = a.overflowRight + a.overflowLeft + Math.max(0, a.scrollWidth - viewportWidth) | |
| const bScore = b.overflowRight + b.overflowLeft + Math.max(0, b.scrollWidth - viewportWidth) | |
| return bScore - aScore || a.width - b.width | |
| }) | |
| console.log(`Wide elements: ${rawMatches.length}`) | |
| console.log(`Clipped (skipped): ${rawMatches.filter((x) => x.clipped).length}`) | |
| console.log(`Highlighted: ${offenders.length}`) | |
| if (!offenders.length) { | |
| console.warn('No uncontained wide elements found.') | |
| window.__overflowOffenders = [] | |
| return [] | |
| } | |
| console.group(`Top ${Math.min(20, offenders.length)} offender(s)`) | |
| offenders.slice(0, 20).forEach((item, index) => { | |
| console.group(`${index + 1}. ${item.selector}`) | |
| console.log('Element:', item.el) | |
| console.log('Box:', `${item.width} x ${item.height}`) | |
| console.log('Scroll width:', item.scrollWidth) | |
| console.log('Past right edge:', `${Math.round(item.overflowRight)}px`) | |
| console.table(item.styles) | |
| console.groupEnd() | |
| }) | |
| console.groupEnd() | |
| offenders.slice(0, 5).forEach((item) => highlightElement(item.el)) | |
| const top = offenders[0] | |
| const label = document.createElement('div') | |
| label.id = 'overflow-debug-label' | |
| label.textContent = `#1 +${Math.round(top.overflowRight)}px (${top.width}px wide)\n${top.selector}` | |
| document.documentElement.appendChild(label) | |
| top.el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' }) | |
| window.__overflowOffenders = offenders | |
| window.__clearOverflowDebug = removeHighlights | |
| window.__inspectOverflowElement = (index = 1) => { | |
| const item = offenders[index - 1] | |
| if (!item) { | |
| console.warn(`No element at index ${index}`) | |
| return null | |
| } | |
| item.el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' }) | |
| console.log(item.selector, item.el) | |
| return item.el | |
| } | |
| overflowDebug.clear = removeHighlights | |
| overflowDebug.inspect = (index = 1) => window.__inspectOverflowElement(index) | |
| overflowDebug.verify = () => { | |
| const marked = document.querySelectorAll(`[${MARKER}]`) | |
| console.log('Marked elements:', marked.length) | |
| marked.forEach((el, i) => { | |
| const rect = el.getBoundingClientRect() | |
| console.log( | |
| i + 1, | |
| getSelector(el), | |
| `${Math.round(rect.width)}x${Math.round(rect.height)}`, | |
| `visible: ${rect.bottom > 0 && rect.top < window.innerHeight}` | |
| ) | |
| }) | |
| return marked | |
| } | |
| console.log('Applied inline highlights to top offenders.') | |
| console.log('Run overflowDebug.verify() if you still do not see red boxes.') | |
| console.log('Run __inspectOverflowElement(1) to jump to #1.') | |
| console.log('Run __clearOverflowDebug() to remove highlights.') | |
| return offenders | |
| } | |
| overflowDebug() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment