Home · Open source · Writing · Contact

04 · Debugging tool

sticky-killer

Names the ancestor element that breaks position: sticky, as a devtools snippet, bookmarklet and extension.

MIT licence · No dependencies · CLI, snippet, bookmarklet, extension

Repository on GitHub

Give it a page file or a URL. It loads the page in a headless Chrome or Edge you already have, finds every sticky element, and tells you which ancestor stops each one. It then applies the fix in memory, scrolls, and measures whether the element now sticks. The repo has no dependencies.

Why this exists

On 30 Sep 2026 I searched GitHub and npm for a tool that finds the ancestor breaking position: sticky and found none (queries: "sticky debugger", "css sticky debug", "position sticky bookmarklet", "sticky overflow ancestor"; the closest hit was a 2017 sticky-viewport bookmarklet that does something else). The question itself is old. A Stack Overflow thread on sticky failing inside an overflow ancestor was reported to have 59,000 views when I logged the idea (Stack Overflow blocks automated fetching, so I did not re-read that count).

I hit the same wall while rebuilding scroll sections on my own site. The answer is almost always "some ancestor has overflow set", and finding which one means clicking up the tree in DevTools and reading computed styles by hand.

Run it

Pick whichever fits. All four run the same code.

  • CLI, on a file or URL. npx github:rishsadh/sticky-killer check page.html, with --json, --no-verify, --viewport 1280x900 and --chrome PATH.
  • DevTools snippet. npx github:rishsadh/sticky-killer snippet, paste into Sources > Snippets, run it on the page.
  • Bookmarklet. npx github:rishsadh/sticky-killer bookmarklet, make a bookmark and paste the output as its URL (about 28 KB).
  • Chrome extension. npx github:rishsadh/sticky-killer extension ./sticky-killer-ext, then chrome://extensions, Developer mode, Load unpacked. Not in the Chrome Web Store.
Terminal output of sticky-killer naming two ancestors that stop a header from sticking, with a CSS fix for each.
sticky-killer naming two ancestors that stop a header from sticking, with a CSS fix for each.

In a browser, the snippet, bookmarklet and extension outline each sticky element (solid) and each blocking ancestor (dashed), print a report to the console, and open a small panel. The panel can preview a fix on the live page and undo it. Close puts every outline and change back.

check exits 1 when something is blocked, 0 when not, 2 on an error, so you can run it in CI against built HTML.

What it checks

For each visible element with position: sticky (shadow DOM included):

  • No inset. None of top, bottom, left or right is set, so sticky does nothing.
  • A scroll container that never scrolls. The nearest ancestor with overflow hidden, scroll, auto or overlay becomes the thing the element sticks to. If it has nothing to scroll, the element is pinned to a box that never moves. Every such ancestor is listed, working outward from the nearest. Body is handled the way the CSS spec does: its overflow passes to the viewport unless html sets overflow too.
  • No room in the parent. The element sits at the edge of its parent, including a flex or grid child stretched to full height.
  • Notes. A real scroller that the element sticks inside (works as designed, just not to the page), and a hidden-overflow box whose content runs past the edge.

The suggested fix is usually overflow: clip, which clips like hidden but does not create a scroll container. Check that your target browsers support it before shipping.

What actually breaks sticky

I measured this instead of repeating it. research/measure-breakers.mjs styles one ancestor at a time, scrolls, and records whether a sticky element pins to the top of the viewport. Run on 30 Sep 2026 with Playwright on Windows. The WebKit column is Playwright's WebKit build, not Safari itself.

Whether a sticky element sticks, by ancestor styling, in Chromium and WebKit
SetupChromium 151.0.7922.34WebKit 26.6
baseline (no ancestor styling)stickssticks
overflow: hiddenbreaksbreaks
overflow: autobreaksbreaks
overflow: scrollbreaksbreaks
overflow: overlaybreaksbreaks
overflow: clipstickssticks
overflow-x: hiddenbreaksbreaks
overflow-x: clipstickssticks
overflow-y: hiddenbreaksbreaks
overflow-x: clip + overflow-y: visiblestickssticks
contain: paintstickssticks
contain: layoutstickssticks
contain: strictstickssticks
contain: contentstickssticks
content-visibility: autostickssticks
transform: translateZ(0)stickssticks
filter: blur(0)stickssticks
backdrop-filter: blur(1px)stickssticks
perspective: 100pxstickssticks
will-change: transformstickssticks
opacity: 0.99stickssticks
position: relativestickssticks
position: absolutestickssticks
isolation: isolatestickssticks
clip-path: inset(0)stickssticks
container-type: inline-sizestickssticks
display: flow-rootstickssticks
display: contentsstickssticks
html + body both overflow-x: hidden (body becomes the scroll container)breaksbreaks
body alone has overflow-x: hidden (propagates to the viewport)stickssticks
html alone has overflow-x: hidden (applies to the viewport)stickssticks
no inset (top/bottom/left/right all auto)breaksbreaks
parent is exactly as tall as the sticky elementbreaksbreaks
flex parent, child stretched by align-items: stretchbreaksbreaks
flex parent with align-self: flex-start on the sticky childstickssticks

The lower half of the table matters as much as the top: transform, filter, contain, will-change, perspective and the other usual suspects did not break sticky in either engine. Overflow (other than visible or clip), a missing inset and a parent with no room did. Firefox is not measured here. Raw results are in research/breakers-chromium.json and research/breakers-webkit.json in the repository.

Limits

  • Not a Safari or Firefox test. The CLI and extension need Chrome or Edge. The snippet and bookmarklet run in any browser that supports them, but the measurements above cover Chromium and WebKit.
  • check works on static HTML. It loads a copy of the page from a temporary file with a base tag, and removes any CSP meta tag so the script can run. Pages that need a login, cookies or same-origin API calls may render differently. Use the snippet or bookmarklet inside your own browser for those.
  • Top frame. Sticky elements inside iframes are not inspected.
  • The scroll probe moves the page and restores the position afterwards. Pages that react to scroll with script (hide-on-scroll headers, for example) can look like they "engage" when sticky is not the cause.
  • A page too short to scroll is reported as not measured, not as broken.
  • The extension toolbar click was not automated. The extension loads in Chromium with no errors and ships the same tested bundle as the snippet, but I clicked nothing in the toolbar.
  • It does not edit your CSS. It prints the fix.

Licence

MIT.

Also on Reidify: reidify.design/open-source/sticky-killer/

By Rish Sadh, founder of Reidify, an AI-first design studio in Mumbai.