Add body class on scroll?

I have an element with position:fixed (a sidebar / side content area). I want to make 2 separate headers (one for scrolled to top, and one for scrolls down).

I need to add a body class (.scrolled-down or something) when the user scrolls down the page, and then remove it when they are scrolled to the top. That’s because the amount of padding for the sidebar is dependent upon the height of the header.

For example:

.scrolled-up #sidecontent {padding-top: 60px) ///to accommodate a bigger header

.scroll-downed #sidecontent {padding-top: 40px} ///for smaller header

I’ve been trying to find some javascript to do what I need, but I end up chasing my tail.

Can anybody help me with this?

1 Like

Hi @jeremy58, you can listen to scroll events where you compare the current window.scrollY to the last one remembered, and toggle() a class on the body accordingly:

let lastY = window.scrollY

window.addEventListener('scroll', () => {
  if (window.scrollY === lastY) {
    return
  }

  document.body.classList.toggle('scrolled-down', window.scrollY > lastY)
  document.body.classList.toggle('scrolled-up', window.scrollY < lastY)
  lastY = window.scrollY
})

Edit: Just added an early return in case the scrollY didn’t change, so that we don’t lose both classes when scrolling horizontally only.

2 Likes

I hope you don’t mind @m3g4p0p,

I know you have posted a straight-forward useable answer, but I just wanted to toy around with some optimizing and throttling — admittedly a more convoluted solution.

Again a link to the Debounce and Throttling article. Note lodash’s _.debounce and _.throttle are recommended in the article, which makes sense :slight_smile:

and an article on ‘Layout Thrashing’

// lodash's _.throttle could be used instead of this.
const throttle = (handler, wait) => {
  let timer = null

  return (event) => {
    if (timer !== null) return

    timer = setTimeout(() => {
      clearTimeout(timer)
      timer = null

      handler.call(event.target, event)
    }, wait)
  }
}

const scrollHandler = (lastY) => {
  
  // returning the handler with 'lastY' held in a closure
  return (event) => {

    // assign window.scrollY to a variable here to reduce layout thrashing
    const { scrollY } = window
    const { body } = document

    if (scrollY === lastY) return

    body.classList.toggle('scrolled-down', scrollY > lastY)
    body.classList.toggle('scrolled-up', scrollY < lastY)

    lastY = scrollY
  }
}

window.addEventListener(
  'scroll', 
  throttle(
    // pass in the initial window.scrollY
    scrollHandler(window.scrollY),
    // delay between handling scroll events in milliseconds
    300 
  )
)

codepen link here

Okay now that we’re talking about throttling… :-) FWIW I think the best way to avoid layout thrashing is to just schedule the callback execution to the next animation frame – i.e.:

const throttle = callback => {
  let isScheduled = false

  return function (event) {
    if (isScheduled) {
      return
    }

    isScheduled = true

    window.requestAnimationFrame(() => {
      callback.call(this, event)
      isScheduled = false
    })
  }
}

This way you can be sure not to “interrupt” the browser during its regular painting business; more extended delays should only be necessary for really expensive calculations.

1 Like

At the bottom of the debounce article he discusses requestAnimationFrame as an other option. I was wondering :slight_smile:

more extended delays should only be necessary for really expensive calculations.

Good Point

Thanks

1 Like

Ah true – I only checked the other link. ^^ Good articles altogether.

1 Like

Thanks for the input, everybody.

The only issue I see is that these seem to add the .scrolled-down class after any amount of scrolling.

My “scrolled-up” header is 190px tall, and my scrolled down header is only about 50px tall.

So, if the user scrolls down < 140px, the .scrolled-down will be added, but the margins (I am modifying with the .scrolled-up/down class) won’t match up…

In essence, I need more grainular control (by pixel height) over when to add the selectors.

You can of course adjust the condition when to toggle the classes, e.g.

document.body.classList.toggle('scrolled-down', window.scrollY > lastY)

might become:

document.body.classList.toggle('scrolled-down', (
  window.scrollY > lastY && 
  window.scrollY >= 140
))
1 Like