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. */