Tim Wijma
Tim Wijma

Reputation: 1775

JavaScript scrollIntoView smooth scroll and offset

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?

Upvotes: 164

Views: 177534

Answers (14)

user23625180
user23625180

Reputation: 11

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' });

Upvotes: 1

MaDragon7
MaDragon7

Reputation: 1313

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.

Upvotes: 8

yangli-io
yangli-io

Reputation: 17354

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"
    });
}

Upvotes: 15

kennethlkf
kennethlkf

Reputation: 135

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"
  });
}

Upvotes: 0

Moshe L
Moshe L

Reputation: 1905

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.

Upvotes: 70

Kuza Grave
Kuza Grave

Reputation: 1574

elementRef.current!.scrollIntoView({ 
     behavior: 'smooth', 
     block: 'center' 
})

Upvotes: 1

The Fool
The Fool

Reputation: 20527

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.

Upvotes: 9

S&#248;ren D. Pt&#230;us
S&#248;ren D. Pt&#230;us

Reputation: 4542

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

Upvotes: 238

laktak
laktak

Reputation: 60043

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.

Upvotes: 83

Cristian Ceamatu
Cristian Ceamatu

Reputation: 1

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>).

Upvotes: 0

Vasiliy Artamonov
Vasiliy Artamonov

Reputation: 1057

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>

Upvotes: 1

Yulian
Yulian

Reputation: 6769

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
  }

Upvotes: 9

Felipe C.
Felipe C.

Reputation: 181

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;
}

Upvotes: 12

ekfuhrmann
ekfuhrmann

Reputation: 1729

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!

Upvotes: 103

Related Questions