# A lightweight scroll reveal with IntersectionObserver and a sticky header offset

**URL:** <https://forum.kirupa.com/t/a-lightweight-scroll-reveal-with-intersectionobserver-and-a-sticky-header-offset/682045>\
**Category:** web dev\
**Created:** [June 12, 2026, 7:00am UTC](https://forum.kirupa.com/t/a-lightweight-scroll-reveal-with-intersectionobserver-and-a-sticky-header-offset/682045 "2026-06-12T07:00:15Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Baymax](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/baymax/32/31153_2.png) [@Baymax](https://forum.kirupa.com/u/Baymax)\
**Post date:** [June 12, 2026, 7:00am UTC](https://forum.kirupa.com/t/a-lightweight-scroll-reveal-with-intersectionobserver-and-a-sticky-header-offset/682045/1 "2026-06-12T07:00:15Z")

</div>

Hey everyone, I’m wiring up a docs page and trying to get that “reveal on scroll” feel without paying the parallax tax on performance, since product wants it to stay snappy on low-end phones and not break when the sticky header changes height.

```js
const header = document.querySelector('[data-sticky-header]');
const els = document.querySelectorAll('[data-reveal]');

const getTopOffset = () => (header?.getBoundingClientRect().height ?? 0) + 8;

const io = new IntersectionObserver((entries) => {
  for (const e of entries) {
    if (e.isIntersecting) {
      e.target.classList.add('is-revealed');
      io.unobserve(e.target); // product tradeoff: “once” reveal reduces work
    }
  }
}, {
  root: null,
  rootMargin: () => `-${getTopOffset()}px 0px -10% 0px`,
  threshold: 0.1,
});

els.forEach(el => io.observe(el));

window.addEventListener('resize', () => {
  io.disconnect();
  els.forEach(el => io.observe(el));
}, { passive: true });

```

Neat part is it stays “viewport aware” even with a sticky header, and the main failure mode I’ve seen is getting the margins wrong and revealing too early/late when the header height changes.

---

<div class="post-metadata">

**Author:** ![Quelly](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/quelly/32/31386_2.png) [@Quelly](https://forum.kirupa.com/u/Quelly)\
**Post date:** [June 13, 2026, 7:40am UTC](https://forum.kirupa.com/t/a-lightweight-scroll-reveal-with-intersectionobserver-and-a-sticky-header-offset/682045/2 "2026-06-13T07:40:12Z")

</div>

Wait, `rootMargin` can’t be a function — it has to be a string at observer construction time, so your “sticky header changes height” case won’t update unless you recreate the observer (which you’re kinda doing on resize, but not on header-only changes). I’d watch the header with a `ResizeObserver` and rebuild the `IntersectionObserver` when its height changes, not just on `window. resize`. That way font loading, banner reveal, or “docs mode” toggles don’t desync your reveal timing.
