Skip to content

Instantly share code, notes, and snippets.

@benkitzelman
Last active June 27, 2025 07:29
Show Gist options
  • Save benkitzelman/68abeb28f14ed7bd5467b7533a428ea9 to your computer and use it in GitHub Desktop.
Save benkitzelman/68abeb28f14ed7bd5467b7533a428ea9 to your computer and use it in GitHub Desktop.
Sticky Position Manager
/**
* StickyPositionManager - allows registration of a Top sticky element,
* under which all other stickies will stack. Parses existing dom els, and
* monitors the dom for added 'managed' els
*
* -- Assumes all sticky elements have style 'position: sticky; top: 0' ---
*
* Usage:
* To apply to child sticky elements - ensure el has attr 'data-sticky-position-managed'
* Then:
* ```StickyPositionManager.registerAsTop('.some-top-sticky-el')```
*
* And all child sticky elements will stick under .some-top-sticky-el
*/
export default class StickyPositionManager {
public static registrationAttribute = 'data-sticky-position-managed';
private static instance: StickyPositionManager;
private topElSelector?: string;
private observer: MutationObserver;
static registerAsTop(cssSelector: string) {
this.instance ||= new StickyPositionManager();
return this.instance.setTopEl(cssSelector);
}
constructor() {
this.observer = this.createObserver();
}
monitorDOM() {
this.observer.observe(document.body, { childList: true, subtree: true });
}
unmonitorDOM() {
this.observer.disconnect();
}
onStickyElAdded(el: HTMLElement): void {
this.stackUnderTopEl(el);
}
private topEl() {
const topEl = this.topElSelector ? $(this.topElSelector) : undefined;
if (topEl?.length !== 0) return topEl;
}
private setTopEl(cssSelector: string) {
this.unmonitorDOM();
this.topElSelector = cssSelector;
$(`[${StickyPositionManager.registrationAttribute}]`).each((_idx, el) => {
this.stackUnderTopEl(el);
});
this.monitorDOM();
return this;
}
private stackUnderTopEl(el: HTMLElement) {
const height = this.topEl()?.outerHeight();
if (!height) return;
const currentTop = $(el).css("top")?.match(/^(\d+)/)?.at(1) || 0;
$(el).css({ top: `${Number(currentTop) + height}px` });
}
private createObserver() {
return new MutationObserver((mutations) => {
const attribute = StickyPositionManager.registrationAttribute;
for (const mutation of mutations) {
for (const node of Array.from(mutation.addedNodes)) {
if (node instanceof HTMLElement) {
// Check if the node itself has the attribute
if (node.hasAttribute(attribute)) {
this.onStickyElAdded(node);
}
// Check all descendants for the attribute
const elements = node.querySelectorAll(`[${attribute}]`);
for (const el of Array.from(elements)) {
this.onStickyElAdded(el as HTMLElement);
}
}
}
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment