September 25, 2025
Why I made my own CMS
My Blog Tour
22min
October 6, 2025
A sticky element seems very intuitive to have except when its content is too long and you can't scroll through them, this post covers how I deal with those situations.
top value for it and position it as sticky, it should "stick" to the top of the scroll view for as long as the main content needs to be. We define the sticky position state of the element by the inset properties (top, bottom, inset-block-start, etc). The general idea is to define these values in a way that allows the element to be overscrolled if its content is too long to fit in the scroll view before reaching its sticky phase, thereby avoiding the dead hang situation we encountered in the example above.
top to , we explicitly allow the element to be sticky at the top only after it has scanned through an additional length of . Creating the always-visible effect we want.top value prevents the element (whose bottom is currently visible due to being fully over-scrolled) from revealing more of its top again.bottom value to the element and switch between top and bottom on scroll direction change, that's a perfect idea. However, it won't work yet, because the original, natural position of the sidebar is still at the very top of the page, which means there has never been a scroll threshold that is above the sidebar to make bottom active, as opposed to how top was active due to the scroll view threshold being below the top of the sidebar as we scroll.bottom to somehow work; one way to make it work is to also shift the natural position of the sidebar by an amount, using an additional spacer above it, which helps push the sidebar down. As long as the scroll view threshold is intersecting with that spacer, it is already technically above the sidebar, which allows bottom to be active instantly when we decide to scroll upward. If that can be done, setting bottom as as well would create the perfect effect we want.bottom sticky anchor is activated, it's perfectly aligned and unrecognisable. To achieve this, we listen to scroll events and update the spacer to grow/shrink based on the scroll distance, ensuring the sidebar's normal flow position matches the current sticky position on screen.if (lastScrollDirection !== currentScrollDirection) { const sidebarContainerTop = sidebarContainer.getBoundingClientRect().y ?? 0; const sidebarElementTop = sidebar.getBoundingClientRect().y ?? 0; let spaceNeeded = Math.max(0, sidebarElementTop - sidebarContainerTop); // ... }
spaceNeeded is the height of the spacer, estimated from its container, in order to "push the sidebar" down to its currently on-screen position. Every time the scroll direction changes, we need to update the spacer before detaching/attaching top or bottom respectively, in order to keep the sidebar stable on screen.let lastScrollY = 0; let lastDirection = 'down'; scrollContainer?.addEventListener('scroll', () => { if (!scrollContainer || !sidebar || !sidebarContainer) return; const newScrollY = Math.max(0, scrollContainer.scrollTop); const direction = newScrollY >= lastScrollY ? 'down' : 'up'; const scrollViewHeight = scrollContainer.clientHeight ?? 0; const elementHeight = sidebar.clientHeight ?? 0; const heightDelta = elementHeight - scrollViewHeight; // Update spacer height when scroll direction changes if (lastDirection !== direction) { const sidebarContainerTop = sidebarContainer.getBoundingClientRect().y ?? 0; const sidebarElementTop = sidebar.getBoundingClientRect().y ?? 0; let elementRelativeTop = Math.max(0, sidebarElementTop - sidebarContainerTop); elementRelativeTop = Math.round(elementRelativeTop); scrollContainer?.style.setProperty('--scroll-y', `${elementRelativeTop}`); } // Set `top` or `bottom` based on scroll direction if (direction === 'down') { sidebar.style.setProperty('top', `${-heightDelta - defaultBottom}px`); sidebar.style.removeProperty('bottom'); } else { sidebar.style.setProperty('bottom', `${-heightDelta - defaultTop}px`); sidebar.style.removeProperty('top'); } lastDirection = direction; lastScrollY = newScrollY; });