JavaScript scrollIntoView smooth scroll and offset
Asked Answered
H

14

151

I have this code for my website:

function clickMe() {
  var element = document.getElementById('about');
  element.scrollIntoView({
    block: 'start',
    behavior: 'smooth',
  });
}

This works pretty nice but I have a fixed header so when the code scrolls to the element the header is in the way.

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

Hooker answered 13/4, 2018 at 15:3 Comment(2)
I fixed this problem by giving the component that scrolls into view some padding-top, which acts as the offset.Aseity
#49859542 use scroll-marginIndoxyl
L
220

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

Leaper answered 16/4, 2018 at 15:23 Comment(4)
getBoundingClientRect().top refers to the distance between the element and the top of the viewport. This is more useful when you're looking to use scrollBy, which scrolls relative to the current position, than with scrollTo, which is absolute. If the element is not in a relatively positioned parent, consider using offsetTop instead. Otherwise, you'll need to add window.pageYOffset to getBoundingClientRect().top to get the correct value for scrollTo.Novobiocin
This worked for me very well, but to make things simpler, replace headerOffset value (45) with document.getElementById('header').offsetHeight. This makes up for any responsive height.Scyphate
Your solution does scroll to random locations depending on where the view is right now.Try
My container have fixed height 100vh so that I cannot use window scroll and I have to use scrollIntoView however there is a sticky header which is hovering the element it scrolled into. what approach can I take in this case?Matchboard
A
103

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!

Akerboom answered 19/4, 2018 at 15:11 Comment(4)
I hope that your const are defined in the namespace of a function and not globally because it would only calculate distances relatively to your viewport at page loadingMother
Worked for me, thanks. Had the issue with Søren D. Ptæus answer either. You had to be at top in order to make it work.Koosis
Thank you for this, I had the same issue since I was using a sticky nav bar, this fixed everything, thanks again!Hiss
This is a very good configurable solution. Customizing the offset especially for smart-headers gives good results.Windproof
U
69

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

element.scrollIntoView({ behavior: 'auto' /*or smooth*/, 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.

EDIT 8.5.22: behavior: instant was used in the past, but removed from browsers.

Uniat answered 17/11, 2019 at 9:21 Comment(3)
behavior Optional Defines the transition animation. One of auto or smooth. Defaults to auto. Where does the "instant" come from?Ceratodus
Not sure why this has so many upvotes considering the previous comment is correct about the made-up behavior option. 'instant' does not exist. It's either 'auto' or 'smooth'.Atone
instant was used in the past and removed by W3C or similar. I will update the answer.Uniat
T
67

You can use scrollIntoView() like in your example

function clickMe() {
  var element = document.getElementById('about');
  element.scrollIntoView({
    block: 'start',
    behavior: 'smooth',
  });
}

if you add scroll-margin with the height of the header to the target element (about):

.about {
  scroll-margin: 100px;
}

Nothing else is needed. scroll-margin is supported by all modern browsers.

Tann answered 11/11, 2021 at 20:24 Comment(5)
This is a much simpler solution than the ones above and the best part is, it works!Hecht
This is the best answer - with cleanest solution. Perfect thank you!Insignificant
Such a ingenious approach, thanks!Easel
Awesome! So clean.Gynandry
this is a better answer than the accepted one. Particularly because one can set the scroll-margin in JS before calling scrollIntoView. Works.Valorous
M
14

Søren D. Ptæus's answser is almost right, but it only works when the user is on top. This is because getBoundingClientRect will always get us the relative height and using window.scrollTo with a relative height doesn't work.

ekfuhrmann improved the answer by getting the total height from the body element and calculating the real height. However, I think it can be easier than that, we can simply use the relative position and use window.scrollBy.

Note: Key difference is window.scrollBy

const HEADER_HEIGHT = 45;

function scrollToTargetAdjusted(){
    const element = document.getElementById('targetElement');
    const elementPosition = element.getBoundingClientRect().top;
    const offsetPosition = elementPosition - HEADER_HEIGHT;

    window.scrollBy({
         top: offsetPosition,
         behavior: "smooth"
    });
}
Matroclinous answered 22/5, 2021 at 9:19 Comment(0)
N
12

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;
}
Navarra answered 21/4, 2019 at 17:7 Comment(0)
S
9

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
  }
Shererd answered 6/3, 2020 at 14:28 Comment(2)
Hack but clever!Electrophysiology
Alternatively you can use scroll-margin-top: 45px;Laundress
R
9

There is also scroll-margin and scroll-padding.

For me, scroll-padding is most useful for this kind of stuff.

/* Keyword values */
scroll-padding-top: auto;

/* <length> values */
scroll-padding-top: 10px;
scroll-padding-top: 1em;
scroll-padding-top: 10%;

/* Global values */
scroll-padding-top: inherit;
scroll-padding-top: initial;
scroll-padding-top: unset;

Additionally, you can use smooth-scroll by setting scroll behaviour to smooth.

/* Keyword values */
scroll-behavior: auto;
scroll-behavior: smooth;

/* Global values */
scroll-behavior: inherit;
scroll-behavior: initial;
scroll-behavior: revert;
scroll-behavior: unset;

It's likely not Internet Explorer compatible, though.

Rosellaroselle answered 14/4, 2021 at 18:39 Comment(1)
After lot of search, this helped me! Thanks man!Tomkin
B
8

to prevent any element from intersecting with fixed top. there are actually many ways to do that. recently I use scroll-padding-top in CSS file.

* {
    scroll-behavior: smooth;
    scroll-padding-top: 100px; /* this pixel should match fixed header height */
  }

what do you mean scroll smoothly? just add scroll-behavior: smooth; in CSS.

if what you want is to open a new page and then scroll smoothly, then that's a different approach. you can check my answer for this here

if what you looking for is to check if the element is in the viewport or not, then that's another story. I'm not sure which one you are looking for. if it's this one, please confirm and I will spend more time summarizing the answer for you. I had this issue and I finally solved it.

Beberg answered 17/8, 2022 at 14:35 Comment(0)
M
1

Here is the function that I wrote based on the @ekfuhrmann's answer.
It takes the element that needs to be scrolled to as the first parameter and other options in the form of the object as the second parameter, similar to how the window.scrollTo() function works.

function scrollToTarget(element, options) {
    if (options.headerHeight === undefined) {
        options.headerHeight = 0;
    }

    var elementRect = element.getBoundingClientRect();

    // If an element has 0 height, then it is hidden, do not scroll
    if (elementRect.height == 0) {
        return;
    }

    var offset = elementRect.top - options.headerHeight;

    if (options.block == 'center') {
        // If an element's height is smaller, than the available screen height (without the height of the header), then add the half of the available space
        // to scroll to the center of the screen
        var availableSpace = window.innerHeight - options.headerHeight;
        if (elementRect.height < availableSpace) {
            offset -= (availableSpace - elementRect.height) / 2;
        }
    }

    var optionsToPass = {
        top: offset
    };
    if (options.behavior !== undefined) {
        optionsToPass.behavior = options.behavior
    }

    window.scrollBy(optionsToPass);
}

The main difference is that it uses window.scrollBy() function instead of window.scrollTo(), so that we don't need to call .getBoundingClientRect() on body.

The options parameter can contain a headerHeight field - it can contain the height of the fixed element on the screen, that needs to be ignored when scrolling to the element.

This function can also have a block option, that for now can only accept a single "center" value. When set, the element which is scrolled to will appear in the center of the screen excluding the fixed element height. By default, the scroll will be applied to the element's top.

Usage example

Here we have two overlapping elements with fixed position. Let's imagine the largest of them is not visible on some viewport widths, so we need to dynamically get the available viewport height minus the height of fixed element.

The following example demonstrates, that the element will appear in the center of the available viewport height if the block option is set to "center", similar to how the Element.scrollIntoView() function works.

function scrollToTarget(element, options) {
    if (options.headerHeight === undefined) {
        options.headerHeight = 0;
    }

    var elementRect = element.getBoundingClientRect();

    if (elementRect.height == 0) {
        return;
    }

    var offset = elementRect.top - options.headerHeight;

    if (options.block == 'center') {
        var availableSpace = window.innerHeight - options.headerHeight;
        if (elementRect.height < availableSpace) {
            offset -= (availableSpace - elementRect.height) / 2;
        }
    }

    var optionsToPass = {
        top: offset
    };
    if (options.behavior !== undefined) {
        optionsToPass.behavior = options.behavior
    }

    window.scrollBy(optionsToPass);
}

var headerElements  = [
  document.querySelector('.header__wrap'),
  document.getElementById('wpadminbar')
];
var maxHeaderHeight = headerElements.reduce(function (max, item) {
  return item ? Math.max(max, item.offsetHeight) : max;
}, 0);

document.getElementById('click-me').addEventListener('click', function() {
  scrollToTarget(document.querySelector('.scroll-element'), {
    headerHeight: maxHeaderHeight,
    block: 'center',
    behavior: 'smooth'
  });
});
body {
  margin: 0;
  height: 1000px;
}
#wpadminbar, .header__wrap {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}
#wpadminbar {
  height: 32px;
  background-color: #1d2327;
  z-index: 2;
  opacity: 0.8;
}
.header__wrap {
  margin: 0 15px;
  height: 74px;
  background-color: #436c50;
  z-index: 1;
}
.scroll-element {
  margin-top: 500px;
  padding: 1em;
  text-align: center;
  background-color: #d7d7d7;
}
#click-me {
  margin: 100px auto 0;
  padding: 0.5em 1em;
  display: block;
}
<div id="wpadminbar"></div>
<div class="header__wrap"></div>
<button id="click-me">Click me!</button>
<!-- Some deeply nested HTML element -->
<div class="scroll-element">
  You scrolled to me and now I am in the visual center of the screen. Nice!
</div>
Meliorism answered 14/4, 2021 at 18:25 Comment(0)
A
1
elementRef.current!.scrollIntoView({ 
     behavior: 'smooth', 
     block: 'center' 
})
Antineutron answered 13/1, 2022 at 14:20 Comment(1)
This only works if nothing exact is needed. A quick, dirty but also often working solution.Prisage
T
0

With a very small hack you can make it work with scrollIntoView()

  • Let's say you want to scroll to a section and your elements are in this format:
<section id="about">
 <p>About title</p>
 <p>About description</p>
</section>

<section id="profile">
 <p>About title</p>
 <p>About description</p>
</section>
  • You convert the above code into this:
<section>
 <span className="section-offset" id="about"></span>
 <!-- or <span className="section-offset" id="about" />  for React -->
 <p>About title</p>
 <p>About description</p>
</section>

<section>
 <span className="section-offset" id="profile"></span>
 <p>Profile title</p>
 <p>Profile description</p>
</section>
  • Then in your css you can easily change the offset by using:
.section-offset {
  position: relative;
  bottom: 60px; // <<< your offset here >>>
}

Conclusion:

Move the element selector to a span inside the section, then you can use position: relative on the span (top/bottom placement does not affect other elements on the page) to set the needed offset. If you need bottom offset, place the span element at the end of your section (ex: before the </section>).

Triage answered 1/6, 2021 at 10:46 Comment(0)
S
0

Come across this question and seems scrollBy provides the best flexibility. This is just a minimalistic version based on @yangli-io answer to save you some time and cleaner code.

function scrollIntoViewAdjusted(elem, offset=0){
  window.scrollBy({
    top: elem.getBoundingClientRect().top - offset,
    behavior: "smooth"
  });
}
Sewerage answered 9/8, 2022 at 11:52 Comment(0)
P
0

can keep using scrollIntoView, and add css style scroll-margin in your element . such as

.tableClass {
   scroll-margin-top: 52px;
} 

tableDom.scrollIntoView({ behavior: 'smooth', inline: 'start' });
Pend answered 15/3 at 12:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.