JavaScript scrollIntoView smooth scroll and offset

Solution 1:

Is there a way to have an offset and make it scroll smoothly?

#Yes, but not with scrollIntoView()

The scrollIntoViewOptions of Element.scrollIntoView() do not allow you to use an offset. It is solely useful when you want to scroll to the exact position of the element.

You can however use Window.scrollTo() with options to both scroll to an offset position and to do so smoothly.

If you have a header with a height of 30px for example you might do the following:

function scrollToTargetAdjusted(){
    var element = document.getElementById('targetElement');
    var headerOffset = 45;
    var elementPosition = element.getBoundingClientRect().top;
    var offsetPosition = elementPosition + window.pageYOffset - headerOffset;
  
    window.scrollTo({
         top: offsetPosition,
         behavior: "smooth"
    });
}

This will smoothly scroll to your element just so that it is not blocked from view by your header.

Note: You substract the offset because you want to stop before you scroll your header over your element.

#See it in action

You can compare both options in the snippet below.

<script type="text/javascript">
  function scrollToTarget() {

    var element = document.getElementById('targetElement');
    element.scrollIntoView({
      block: "start",
      behavior: "smooth",
    });
  }

  function scrollToTargetAdjusted() {
        var element = document.getElementById('targetElement');
      var headerOffset = 45;
        var elementPosition = element.getBoundingClientRect().top;
      var offsetPosition = elementPosition + window.pageYOffset - headerOffset;
      
      window.scrollTo({
          top: offsetPosition,
          behavior: "smooth"
      });   
  }

  function backToTop() {
    window.scrollTo(0, 0);
  }
</script>

<div id="header" style="height:30px; width:100%; position:fixed; background-color:lightblue; text-align:center;"> <b>Fixed Header</b></div>

<div id="mainContent" style="padding:30px 0px;">

  <button type="button" onclick="scrollToTarget();">element.scrollIntoView() smooth, header blocks view</button>
  <button type="button" onclick="scrollToTargetAdjusted();">window.scrollTo() smooth, with offset</button>

  <div style="height:1000px;"></div>
  <div id="targetElement" style="background-color:red;">Target</div>
  <br/>
  <button type="button" onclick="backToTop();">Back to top</button>
  <div style="height:1000px;"></div>
</div>

Edit

window.pageYOffset have being added, to fix the problem related to @coreyward comments

Solution 2:

Søren D. Ptæus's answer got me on the right track but I had issues with getBoundingClientRect() when not at the top of the window.

My solution adds a bit more to his to get getBoundingClientRect() working a bit more consistently with more versatility. I used the approach outlined here and implemented it to get this working as intended.

const element = document.getElementById('targetElement');
const offset = 45;
const bodyRect = document.body.getBoundingClientRect().top;
const elementRect = element.getBoundingClientRect().top;
const elementPosition = elementRect - bodyRect;
const offsetPosition = elementPosition - offset;

window.scrollTo({
  top: offsetPosition,
  behavior: 'smooth'
});

Codepen Example

Remember to include the polyfill when implementing this!

Solution 3:

Simple but elegant solution if the element has a small height (shorter than the viewport):

element.scrollIntoView({ behavior: 'instant', block: 'center' });

The block: center will scroll the element so the center of the element is at the vertical center of the viewport, so the top header will not cover it.

Solution 4:

I tried the other solutions, but I was getting some strange behavior. However, this worked for me.

function scrollTo(id) {
    var element = document.getElementById(id);
    var headerOffset = 60;
    var elementPosition = element.offsetTop;
    var offsetPosition = elementPosition - headerOffset;
    document.documentElement.scrollTop = offsetPosition;
    document.body.scrollTop = offsetPosition; // For Safari
}

and the style:

html {
    scroll-behavior: smooth;
}

Solution 5:

I know this is a hack and definitely is something that you should use with caution, but you can actually add a padding and a negative margin to the element. I cannot guarantee that it would work for you as I don't have your markup and code, but I had a similar issue and used this workaround to solve it.

Say your header is 30px and you want an offset of 15px, then:

  #about {
     padding-top: 45px; // this will allow you to scroll 15px below your 30px header
     margin-top: -45px; // and this will make sure that you don't change your layout because of it
  }