ChatGPT解决这个技术问题 Extra ChatGPT

scrollIntoView Scrolls just too far

I have a page where a scroll bar containing table rows with divs in them is dynamically generated from the database. Each table row acts like a link, sort of like you'd see on a YouTube playlist next to the video player.

When a user visits the page, the option they are on is supposed to go to the top of the scrolling div. This functionality is working. The issue is that it goes just a tad too far. Like the option they are on is about 10px too high. So, the page is visited, the url is used to identify which option was selected and then scrolls that option to the top of the scrolling div. Note: This is not the scroll bar for the window, it is a div with a scrollbar.

I am using this code to make it move the selected option to the top of the div:

var pathArray = window.location.pathname.split( '/' );

var el = document.getElementById(pathArray[5]);

el.scrollIntoView(true);

It moves it to the top of the div but about 10 pixels too far up. Anyone know how to fix that?

The lack of offset configuration for scrollIntoView is troubling.
@mystrdat There are new CSS properties that deal with this, scroll-margin and scroll-padding.

A
Arseniy-II

Smooth scroll to the proper position

Get correct y coordinate and use window.scrollTo({top: y, behavior: 'smooth'})

const id = 'profilePhoto';
const yOffset = -10; 
const element = document.getElementById(id);
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;

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

This is the most correctly answer - we can easly find position of scroll with jQuery('#el').offset().top and then to use window.scrollTo
+1 for this solution. It helped me to scroll to the element correctly when I had a container positioned relative with a top offset.
Anyone coming here using HashLink with React Router, this is what worked for me. In the scroll prop on your HashLink, scroll={el => { const yCoordinate = el.getBoundingClientRect().top + window.pageYOffset; const yOffset = -80; window.scrollTo({ top: yCoordinate + yOffset, behavior: 'smooth' }); }} -- where the offset is 10-15 pixels more than the size of your header just for better spacing
This is the first answer that actually helped me
perfect. Thanks a lot!
R
Rohit Sharma

Position Anchor By Absolute Method

Another way to do this is to position your anchors exactly where you want on the page rather than relying on scrolling by offset. I find it allows better control for each element (eg. if you want a different offset for certain elements), and may also be more resistant to browser API changes/differences.

<div id="title-element" style="position: relative;">
  <div id="anchor-name" style="position: absolute; top: -100px; left: 0"></div>
</div>

Now the offset is specified as -100px relative to the element. Create a function to create this anchor for code reuse, or if you are using a modern JS framework such as React do this by creating a component that renders your anchor, and pass in the anchor name and alignment for each element, which may or may not be the same.

Then just use :

const element = document.getElementById('anchor-name')
element.scrollIntoView({ behavior: 'smooth', block: 'start' });

For smooth scrolling with an offset of 100px.


This is by far the best way to implement this smoothly and simply.
The best and most native method I think.
This is so simple and slick. Many of the others just add unneeded JS.
This is especially useful when a "page top" type link is below a nav bar, and you want to show the nav, but don't want any other links offset
you can achieve the same using negative margin-top, you avoid of wrapping the element along with another with position relative
f
fred727

You can do it in two steps :

el.scrollIntoView(true);
window.scrollBy(0, -10); // Adjust scrolling with a negative value here

If the scrollIntoView() hasn't finished scrolling before scrollBy() runs, the latter scroll will cancel the former. At least on Chrome 71.
In this case use the setTimeout as seen in an answer below: stackoverflow.com/a/51935129/1856258 .. For me it works without Timeout. Thanks!
is there a way to directly inject a pixel's corrector into scrollIntoView in order to make the application move directly to the relevant place? Like scrollIntoView({adjustment:y}), I think it should be possible with a custom code at the end
M
Meta-Knight

If it's about 10px, then I guess you could simply manually adjust the containing div's scroll offset like that:

el.scrollIntoView(true);
document.getElementById("containingDiv").scrollTop -= 10;

Will this have the same animated effect as scrollIntoView?
@thomas AFAIK, the plain DOM scrollIntoView function does not cause animation. Are you talking about the scrollIntoView jQuery plugin?
That worked man, except it was the wrong direction so all I had to do was change it from += 10 to -= 10 and now it's loading just right, thanks a lot for the help!!!!
Yep. I was confused. I thought it animated. Sorry!
It can animate, just add el.scrollIntoView({ behavior: 'smooth' });. Support isn't great though!
I
Imtiaz Shakil Siddique

I solved this problem by using,

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

This makes the element appear in the center after scrolling, so I don't have to calculate yOffset.

Hope it helps...


You should use scrollTo not on element but on window
@Arseniy-II Thanks for pointing that out!!. I missed that part!
does the block helps for top?
My scroll was just off a little bit! it worked with this! thanks!
Nice this should be top comment!
s
schettino72

This works for me in Chrome (With smooth scrolling and no timing hacks)

It just moves the element, initiates the scroll, then moves it back.

There is no visible "popping" if the element is already on the screen.

pos = targetEle.style.position;
top = targetEle.style.top;
targetEle.style.position = 'relative';
targetEle.style.top = '-20px';
targetEle.scrollIntoView({behavior: 'smooth', block: 'start'});
targetEle.style.top = top;
targetEle.style.position = pos;

This is the best solution... Wish it was marked as answer. Would have saved a couple hours.
Tested it and it works out of the box. Not a hack and works great! Vote this one up!
Amazing. This way (storing style.top, setting to -x and reapplying it later after scrollIntoView is the only thing that works in Blazor and actually works. Using it with Virtualize component to show lot of data. Was shocked to learn there isn't scrolling supported by any means!! Thanks
Perfect!! I had to login to like you :)))
F
Flimm

CSS scroll-margin and scroll-padding

You might want to have a look at new CSS properties scroll-padding and scroll-margin. You can use scroll-padding for the scrolling container (html in this case), and scroll-margin for the element within the container.

For your example, you would want to add scroll-margin-top for the element that you want to scroll into view, like this:

.example {
  scroll-margin-top: 10px;
}

This affects scrollIntoView code, like this code:

const el = document.querySelector(".example");
el.scrollIntoView({block: "start", behavior: "smooth"});

This will cause the viewport to scroll to align the top border of the viewport with the top border of the element, but with 10px of additional space. In other words, these properties of the element are taken into account:

padding-top

border-top

scroll-margin-top

(and not margin-top)

In addition, if the html element has scroll-padding-top set, then that is taken into account too.


If you're using a framework like Vue, you might want to make sure that all changes to the DOM are finished before running scrollIntoView by using something like Vue.nextTick(() => el.scrollIntoView())
This is an awesome solution.. which is completely made sense :) Thanks @Flimm
perfect thanks mate! The selected answer didn't quite work for me.
Best answer in 2021!
Another troublshooting tip: if you're loading a URL with a fragment (for example example.com/#test) , then the browser will scroll to the element with that id or name when the page has loaded, but not before all AJAX has finished loading. If you're causing layout shifts after the page has loaded, navigating to a URL with a fragment might not scroll to the expected position.
D
Diego Fortes

Fix it in 20 seconds:

This solution belongs to @Arseniy-II, I have just simplified it into a function.

function _scrollTo(selector, yOffset = 0){
  const el = document.querySelector(selector);
  const y = el.getBoundingClientRect().top + window.pageYOffset + yOffset;

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

Usage (you can open up the console right here in StackOverflow and test it out):

_scrollTo('#question-header', 0);

I'm currently using this in production and it is working just fine.


This worked a treat for me, I also added a line to dynamically determine the offset required. My content was sitting under the nav, so I fetched the nav height to be passed in as yOffset const yOffset = document.getElementById('nav_id').getBoundingClientRect().height I also had to modify slightly to - the yOffset rather than +, as my content was scrolled too far up the page (under the nav)
A
AmerllicA

Another solution is to use "offsetTop", like this:

var elementPosition = document.getElementById('id').offsetTop;

window.scrollTo({
  top: elementPosition - 10, //add your necessary value
  behavior: "smooth"  //Smooth transition to roll
});

P
PrimaryW

Building on an earlier answer, I am doing this in an Angular5 project.

Started with:

// el.scrollIntoView(true);
el.scrollIntoView({
   behavior: 'smooth',
   block: 'start'
});
window.scrollBy(0, -10); 

But this gave some problems and needed to setTimeout for the scrollBy() like this:

//window.scrollBy(0,-10);
setTimeout(() => {
  window.scrollBy(0,-10)
  }, 500);

And it works perfectly in MSIE11 and Chrome 68+. I have not tested in FF. 500ms was the shortest delay I would venture. Going lower sometimes failed as the smooth scroll had not yet completed. Adjust as required for your own project.

+1 to Fred727 for this simple but effective solution.


This feels a bit janky to smooth scroll to an element and then scroll up a bit after.
G
Georgy Petukhov

Assuming you want to scroll to the divs that are all at the same level in DOM and have class name "scroll-with-offset", then this CSS will solve the issue:

.scroll-with-offset {    
  padding-top: 100px;
  margin-bottom: -100px;
}

The offset from the top of the page is 100px. It will only work as intended with block: 'start':

element.scrollIntoView({ behavior: 'smooth', block: 'start' });

What's happening is that the divs' top point is at the normal location but their inner contents start 100px below the normal location. That's what padding-top:100px is for. margin-bottom: -100px is to offset the below div's extra margin. To make the solution complete also add this CSS to offset the margins/paddings for the top-most and bottom-most divs:

.top-div {
  padding-top: 0;
}
.bottom-div {
  margin-bottom: 0;
}

F
Fernando Brandao

I've got this and it works brilliantly for me:

// add a smooth scroll to element
scroll(el) {
el.scrollIntoView({
  behavior: 'smooth',
  block: 'start'});

setTimeout(() => {
window.scrollBy(0, -40);
}, 500);}

Hope it helps.


doesnt work in vue, sometimes the scroll posisition missed
x
xiawi

So, perhaps this is a bit clunky but so far so good. Im working in angular 9.

file .ts

scroll(el: HTMLElement) {
  el.scrollIntoView({ block: 'start',  behavior: 'smooth' });   
}

file .html

<button (click)="scroll(target)"></button>
<div  #target style="margin-top:-50px;padding-top: 50px;" ></div>

I adjust the offset with margin and padding top.

Saludos!


N
Nicholas Murray

You can also use the element.scrollIntoView() options

el.scrollIntoView(
  { 
    behavior: 'smooth', 
    block: 'start' 
  },
);

which most browsers support


However this doesn't support any offset option.
j
jfbloom22

Solution if you are using Ionic Capacitor, Angular Material, and need to support iOS 11.

                document.activeElement.parentElement.parentElement.scrollIntoView({block: 'center', behavior: 'smooth'}); 

The key is to scroll to the parent of the parent which is the wrapper around the input. This wrapper includes the label for the input which is now no longer cut off.

If you only need to support iOS 14 the "block" center param actually works, so this is sufficient:

                document.activeElement.scrollIntoView({block: 'center', behavior: 'smooth'}); 

M
Malik Zahid

The simplest way to do this,

html, body {
    scroll-behavior: smooth;
}

    html [id], body [id] {
        scroll-margin: 50px !important;
    }

with your provided code

var pathArray = window.location.pathname.split( '/' );
var el = document.getElementById(pathArray[5]);
el.style.scrollMargin = 50px !important;
el.scrollIntoView(true);

I
Ibrahim Al Masri

Found a workaround solution. Say that you want to scroll to an div, Element here for example, and you want to have a spacing of 20px above it. Set the ref to a created div above it:

<div ref={yourRef} style={{position: 'relative', bottom: 20}}/> <Element />

Doing so will create this spacing that you want.

If you have a header, create an empty div as well behind the header and assign to it a height equal to the height of the header and reference it.


L
Linh

My main idea is creating a tempDiv above the view which we want to scroll to. It work well without lagging in my project.

scrollToView = (element, offset) => {
    var rect = element.getBoundingClientRect();
    var targetY = rect.y + window.scrollY - offset;

    var tempDiv;
    tempDiv = document.getElementById("tempDiv");
    if (tempDiv) {
        tempDiv.style.top = targetY + "px";
    } else {
        tempDiv = document.createElement('div');
        tempDiv.id = "tempDiv";
        tempDiv.style.background = "#F00";
        tempDiv.style.width = "10px";
        tempDiv.style.height = "10px";
        tempDiv.style.position = "absolute";
        tempDiv.style.top = targetY + "px";
        document.body.appendChild(tempDiv);
    }

    tempDiv.scrollIntoView({ behavior: 'smooth', block: 'start' });
}

Example using

onContactUsClick = () => {
    this.scrollToView(document.getElementById("contact-us"), 48);
}

Hope it help


k
kounex

Based on the answer of Arseniy-II: I had the Use-Case where the scrolling entity was not window itself but a inner Template (in this case a div). In this scenario we need to set an ID for the scrolling container and get it via getElementById to use its scrolling function:

<div class="scroll-container" id="app-content">
  ...
</div>
const yOffsetForScroll = -100
const y = document.getElementById(this.idToScroll).getBoundingClientRect().top;
const main = document.getElementById('app-content');
main.scrollTo({
    top: y + main.scrollTop + yOffsetForScroll,
    behavior: 'smooth'
  });

Leaving it here in case someone faces a similar situation!


r
rubeus

Here's my 2 cents.

I've also had the issue of the scrollIntoView scrolling a bit past the element, so I created a script (native javascript) that prepends an element to the destination, positioned it a bit to the top with css and scrolled to that one. After scrolling, I remove the created elements again.

HTML:

//anchor tag that appears multiple times on the page
<a href="#" class="anchors__link js-anchor" data-target="schedule">
    <div class="anchors__text">
        Scroll to the schedule
    </div>
</a>

//The node we want to scroll to, somewhere on the page
<div id="schedule">
    //html
</div>

Javascript file:

(() => {
    'use strict';

    const anchors = document.querySelectorAll('.js-anchor');

    //if there are no anchors found, don't run the script
    if (!anchors || anchors.length <= 0) return;

    anchors.forEach(anchor => {
        //get the target from the data attribute
        const target = anchor.dataset.target;

        //search for the destination element to scroll to
        const destination = document.querySelector(`#${target}`);
        //if the destination element does not exist, don't run the rest of the code
        if (!destination) return;

        anchor.addEventListener('click', (e) => {
            e.preventDefault();
            //create a new element and add the `anchors__generated` class to it
            const generatedAnchor = document.createElement('div');
            generatedAnchor.classList.add('anchors__generated');

            //get the first child of the destination element, insert the generated element before it. (so the scrollIntoView function scrolls to the top of the element instead of the bottom)
            const firstChild = destination.firstChild;
            destination.insertBefore(generatedAnchor, firstChild);

            //finally fire the scrollIntoView function and make it animate "smoothly"
            generatedAnchor.scrollIntoView({
                behavior: "smooth",
                block: "start",
                inline: "start"
            });

            //remove the generated element after 1ms. We need the timeout so the scrollIntoView function has something to scroll to.
            setTimeout(() => {
                destination.removeChild(generatedAnchor);
            }, 1);
        })
    })
})();

CSS:

.anchors__generated {
    position: relative;
    top: -100px;
}

Hope this helps anyone!


J
Jérémy MARQUER

I add this css tips for those who not resolved this issue with solutions above :

#myDiv::before {
  display: block;
  content: " ";
  margin-top: -90px; // adjust this with your header height
  height: 90px; // adjust this with your header height
  visibility: hidden;
}

D
Dmitry Zakharov

UPD: I've created an npm package that works better than the following solution and easier to use.

My smoothScroll function

I've taken the wonderful solution of Steve Banton and wrote a function that makes it more convenient to use. It'd be easier just to use window.scroll() or even window.scrollBy(), as I've tried before, but these two have some problems:

Everything becomes junky after using them with a smooth behavior on.

You can't prevent them anyhow and have to wait till the and of the scroll. So I hope my function will be useful for you. Also, there is a lightweight polyfill that makes it work in Safari and even IE.

Here is the code

Just copy it and mess up with it how ever you want.

import smoothscroll from 'smoothscroll-polyfill';

smoothscroll.polyfill();

const prepareSmoothScroll = linkEl => {
  const EXTRA_OFFSET = 0;

  const destinationEl = document.getElementById(linkEl.dataset.smoothScrollTo);
  const blockOption = linkEl.dataset.smoothScrollBlock || 'start';

  if ((blockOption === 'start' || blockOption === 'end') && EXTRA_OFFSET) {
    const anchorEl = document.createElement('div');

    destinationEl.setAttribute('style', 'position: relative;');
    anchorEl.setAttribute('style', `position: absolute; top: -${EXTRA_OFFSET}px; left: 0;`);

    destinationEl.appendChild(anchorEl);

    linkEl.addEventListener('click', () => {
      anchorEl.scrollIntoView({
        block: blockOption,
        behavior: 'smooth',
      });
    });
  }

  if (blockOption === 'center' || !EXTRA_OFFSET) {
    linkEl.addEventListener('click', () => {
      destinationEl.scrollIntoView({
        block: blockOption,
        behavior: 'smooth',
      });
    });
  }
};

export const activateSmoothScroll = () => {
  const linkEls = [...document.querySelectorAll('[data-smooth-scroll-to]')];

  linkEls.forEach(linkEl => prepareSmoothScroll(linkEl));
};

To make a link element just add the following data attribute:

data-smooth-scroll-to="element-id"

Also you can set another attribute as an addtion

data-smooth-scroll-block="center"

It represents the block option of the scrollIntoView() function. By default, it's start. Read more on MDN.

Finally

Adjust the smoothScroll function to your needs.

For example, if you have some fixed header (or I call it with the word masthead) you can do something like this:

const mastheadEl = document.querySelector(someMastheadSelector);

// and add it's height to the EXTRA_OFFSET variable

const EXTRA_OFFSET = mastheadEl.offsetHeight - 3;

If you don't have such a case, then just delete it, why not :-D.


R
Radu Ionescu

I tried using some of the answers from above, but the scrolling would not work. After some digging around I noticed that the scroll positions were in fact very odd (even negative values) and it was caused by how Angular was rendering the pages in the router-outlet.

My solution was to compute the scroll position of the element starting from the top of the page. I placed an element with id start-of-page at the beginning of the template and I obtained the scroll amount by subtracting the target fragment position.

 ngAfterViewInit(): void {
    this.route.fragment.subscribe(fragment => {
      try {
        // @ts-ignore
        // document.querySelector('#hero').scrollIntoView({behavior:smooth, block: "start", inline: "start"});
        // @ts-ignore
        // document.querySelector('#' + fragment).scrollIntoView({behavior:smooth, block: "start", inline: "start"});
        this._scrollTo('#' + fragment,0);
      } catch (e) { };
    });
  }

private _scrollTo(selector: any, yOffset = 0){
    const base = document.querySelector('#start-of-page');
    const el = document.querySelector(selector);
    // @ts-ignore
    const y = el.getBoundingClientRect().top - base.getBoundingClientRect().top; 
    window.scrollTo({top: y, behavior: 'smooth'});
  }

A
Anomaly

One solution is to create an invisible temporary element with an offset relative to the target, scroll to it, and then delete it. It's perhaps not ideal, but other solutions weren't working for me in the context I was working in. For example:

const createOffsetElement = (element) => {
    const offsetElement = document.createElement('div');
    offsetElement.style = `
        position: relative;
        top: -10em;
        height: 1px;
        width: 1px;
        visibility: hidden;
    `;
    const inputWrapper = element.closest('.ancestor-element-class');
    inputWrapper.appendChild(offsetElement);
    return offsetElement;
};

const scrollTo = (element) => {
    const offsetElement = createOffsetElement(element);
    offsetElement.scrollIntoView({
        behavior: 'smooth',
    });
    offsetElement.remove();
};

M
Martin Francis

For an element in a table row, use JQuery to get the row above the one you want, and simply scroll to that row instead.

Suppose I have multiple rows in a table, some of which should be reviewed by and admin. Each row requiring review has both and up and a down arrow to take you to the previous or next item for review.

Here's a complete example that should just run if you make a new HTML document in notepad and save it. There's extra code to detect the top and bottom of our items for review so we don't throw any errors.

<html>
<head>
    <title>Scrolling Into View</title>
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <style>
        div.scroll { height: 6em; width: 20em; overflow: auto; }
        thead th   { position: sticky; top: -1px; background: #fff; }
        .up, .down { cursor: pointer; }
        .up:hover, .down:hover { color: blue; text-decoration:underline; }
    </style>
</head>
<body>
<div class='scroll'>
<table border='1'>
    <thead>
        <tr>
            <th>Review</th>
            <th>Data</th>
        </tr>
    </thead>
    <tbody>
        <tr id='row_1'>
            <th></th>
            <td>Row 1 (OK)</td>
        </tr>
        <tr id='row_2'>
            <th></th>
            <td>Row 2 (OK)</td>
        </tr>
        <tr id='row_3'>
            <th id='jump_1'><span class='up'>UP</span> <span class='down'>DN</span></th>
            <td>Row 3 (REVIEW)</td>
        </tr>
        <tr id='row_4'>
            <th></th>
            <td>Row 4 (OK)</td>
        </tr>
        <tr id='row_5'>
            <th id='jump_2'><span class='up'>UP</span> <span class='down'>DN</span></th>
            <td>Row 5 (REVIEW)</td>
        </tr>
        <tr id='row_6'>
            <th></th>
            <td>Row 6 (OK)</td>
        </tr>
        <tr id='row_7'>
            <th></th>
            <td>Row 7 (OK)</td>
        </tr>
        <tr id='row_8'>
            <th id='jump_3'><span class='up'>UP</span> <span class='down'>DN</span></th>
            <td>Row 8 (REVIEW)</td>
        </tr>
        <tr id='row_9'>
            <th></th>
            <td>Row 9 (OK)</td>
        </tr>
        <tr id='row_10'>
            <th></th>
            <td>Row 10 (OK)</td>
        </tr>
    </tbody>
</table>
</div>
<script>
$(document).ready( function() {
    $('.up').on('click', function() {
        var id = parseInt($(this).parent().attr('id').split('_')[1]);
        if (id>1) {
            var row_id = $('#jump_' + (id - 1)).parent().attr('id').split('_')[1];
            document.getElementById('row_' + (row_id-1)).scrollIntoView({behavior: 'smooth', block: 'start'});
        } else {
            alert('At first');
        }
    });

    $('.down').on('click', function() {
        var id = parseInt($(this).parent().attr('id').split('_')[1]);
        if ($('#jump_' + (id + 1)).length) {
            var row_id = $('#jump_' + (id + 1)).parent().attr('id').split('_')[1];
            document.getElementById('row_' + (row_id-1)).scrollIntoView({behavior: 'smooth', block: 'start'});
        } else {
            alert('At last');
        }
    });
});
</script>
</body>
</html>