Skip to content

Instantly share code, notes, and snippets.

@mikestecker
Last active June 10, 2026 16:51
Show Gist options
  • Select an option

  • Save mikestecker/1a541d69891cb8cfd4484e56a13b6176 to your computer and use it in GitHub Desktop.

Select an option

Save mikestecker/1a541d69891cb8cfd4484e56a13b6176 to your computer and use it in GitHub Desktop.
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