v144 · CSS · Container Queries · demo

Overflow Indicator

Top and bottom fade overlays that appear exactly when the scroll container has content beyond the visible edge — driven entirely by @scroll-state(scrolled: top) and @scroll-state(scrolled: bottom) container queries. Zero scroll event listeners.

Checking @scroll-state support…
Live overflow indicator

Scroll the list above. The top fade appears once you scroll away from the top; the bottom fade disappears once you reach the bottom.

CSS vs JavaScript approach
/* Chrome 144+ — pure CSS, no JavaScript */ .scroll-container-wrap { position: relative; container-type: scroll-state; container-name: scrollbox; } .fade-top { opacity: 0; } /* hidden by default */ .fade-bottom { opacity: 1; } /* visible by default */ /* Show top fade when NOT at the top of scrollbox */ @container scrollbox not scroll-state(scrolled: top) { .fade-top { opacity: 1; } } /* Hide bottom fade when AT the bottom */ @container scrollbox scroll-state(scrolled: bottom) { .fade-bottom { opacity: 0; } } /* That's it. No resize observers, no scroll events, no rAF. */
/* JavaScript equivalent — what @scroll-state replaces */ const container = document.querySelector('.scroll-area'); const fadeTop = document.querySelector('.fade-top'); const fadeBottom = document.querySelector('.fade-bottom'); function updateFades() { const { scrollTop, scrollHeight, clientHeight } = container; const atTop = scrollTop === 0; const atBottom = Math.abs(scrollTop + clientHeight - scrollHeight) < 1; fadeTop.style.opacity = atTop ? 0 : 1; fadeBottom.style.opacity = atBottom ? 0 : 1; } container.addEventListener('scroll', updateFades, { passive: true }); window.addEventListener('resize', updateFades); new ResizeObserver(updateFades).observe(container); updateFades(); // initial state /* Downsides: main-thread scroll listener, needs ResizeObserver, must remember to clean up if container is removed. */

see also