168

I have 20 list items inside of a div that can only show 5 at a time. What is a good way to scroll to item #10, and then item #20? I know the height of all the items.

The scrollTo plugin does this, but its source is not super easy to understand without really getting into it. I don't want to use this plugin.

Let's say I have a function that takes 2 elements $parentDiv, $innerListItem, neither $innerListItem.offset().top nor $innerListItem.positon().top gives me the correct scrollTop for $parentDiv.

Chait
  • 1,018
  • 2
  • 16
  • 27
mkoryak
  • 54,015
  • 59
  • 193
  • 252
  • Removed my answer due to your update. But trust me, having had this same attitude before, there are lots of browser quirks with position and margins, etc...the plugin is a much simpler route and less time consuming, and only 3k **before** GZip. – Nick Craver Feb 27 '10 at 02:39
  • 1
    Use the scrollTo plugin anyway. It's not difficult. `$("#container").scrollTo(target, duration, options)`. Target is just an `#anchor`. Done. – Ryan McGeary Feb 27 '10 at 02:43
  • it seems like there should be knowledge out there to do this without a plugin. tomorrow ill start reading the source – mkoryak Feb 27 '10 at 02:46
  • 5
    @mkoryak - The knowledge is out there, but when you account for the browser quirks, you basically end up with... **the .scrollTo plugin**, so why re-invent the wheel all the time? – Nick Craver Feb 27 '10 at 02:54
  • @Nick - scrollTo does much more then what i require, so i can end up with the .scrollTo plugin that does exactly what i want if i read the source – mkoryak Feb 27 '10 at 07:06
  • 1
    IMHO, you'll probably end up with the exact subset of what you need out of .scrollTo *today*. But in a few months time, while .scrollTo has been updated to IE9, FF4 and Chrome 6, *you* will have to start over... 3k seems like a very low price to avoid this. – Wim Feb 27 '10 at 18:14
  • yeah i guess you have a point. I am writing my own plugin, and id rather not have any dependancies but it might be more trouble then its worth. granted scrollTo hasnt been updated in a while – mkoryak Feb 28 '10 at 01:21

7 Answers7

241

The $innerListItem.position().top is actually relative to the .scrollTop() of its first positioned ancestor. So the way to calculate the correct $parentDiv.scrollTop() value is to begin by making sure that $parentDiv is positioned. If it doesn't already have an explicit position, use position: relative. The elements $innerListItem and all its ancestors up to $parentDiv need to have no explicit position. Now you can scroll to the $innerListItem with:

// Scroll to the top
$parentDiv.scrollTop($parentDiv.scrollTop() + $innerListItem.position().top);

// Scroll to the center
$parentDiv.scrollTop($parentDiv.scrollTop() + $innerListItem.position().top
    - $parentDiv.height()/2 + $innerListItem.height()/2);
Glenn Moss
  • 6,441
  • 6
  • 27
  • 23
157

This is my own plugin (will position the element in top of the the list. Specially for overflow-y : auto. May not work with overflow-x!):

NOTE: elem is the HTML selector of an element which the page will be scrolled to. Anything supported by jQuery, like: #myid, div.myclass, $(jquery object), [dom object], etc.

jQuery.fn.scrollTo = function(elem, speed) { 
    $(this).animate({
        scrollTop:  $(this).scrollTop() - $(this).offset().top + $(elem).offset().top 
    }, speed == undefined ? 1000 : speed); 
    return this; 
};

If you don't need it to be animated, then use:

jQuery.fn.scrollTo = function(elem) { 
    $(this).scrollTop($(this).scrollTop() - $(this).offset().top + $(elem).offset().top); 
    return this; 
};

How to use:

$("#overflow_div").scrollTo("#innerItem");
$("#overflow_div").scrollTo("#innerItem", 2000); //custom animation speed 

Note: #innerItem can be anywhere inside #overflow_div. It doesn't really have to be a direct child.

Tested in Firefox (23) and Chrome (28).

If you want to scroll the whole page, check this question.

Chait
  • 1,018
  • 2
  • 16
  • 27
lepe
  • 22,543
  • 9
  • 85
  • 99
  • what is `elem` ?? you don't explain it at all. – vsync Nov 13 '14 at 16:03
  • You get better mileage/non conflict if you swap $ in the fn.scrollTo to jQuery instead – Barry Carlyon Sep 11 '15 at 12:03
  • Big flaw with this, your not containing the scope: `$(this).scrollTop($(this).scrollTop() - $(this).offset().top + $(elem, $(this)).offset().top);` - regards – Luke Snowden Oct 27 '15 at 10:32
  • 1
    If you want to leave some top margin between the page and the element (which may look better in some cases), subtract, for example, 10px, like: `$(this).scrollTop() - $(this).offset().top + $(elem).offset().top - 10`. – lepe May 19 '16 at 03:22
  • If you are having position differences between Firefox and Chrome (or other browsers), check your html code (see [this bug report](https://bugs.jquery.com/ticket/13696)) – lepe May 19 '16 at 03:23
  • Works nicely in IE11 too <3 – gordon Jul 13 '16 at 15:20
35

I've adjusted Glenn Moss' answer to account for the fact that overflow div might not be at the top of the page.

parentDiv.scrollTop(parentDiv.scrollTop() + (innerListItem.position().top - parentDiv.position().top) - (parentDiv.height()/2) + (innerListItem.height()/2)  )

I was using this on a google maps application with a responsive template. On resolution > 800px, the list was on the left side of the map. On resolution < 800 the list was below the map.

cracker
  • 4,808
  • 3
  • 18
  • 37
KPheasey
  • 1,696
  • 15
  • 21
6

The above answers will position the inner element at the top of the overflow element even if it's in view inside the overflow element. I didn't want that so I modified it to not change the scroll position if the element is in view.

jQuery.fn.scrollTo = function(elem, speed) {
    var $this = jQuery(this);
    var $this_top = $this.offset().top;
    var $this_bottom = $this_top + $this.height();
    var $elem = jQuery(elem);
    var $elem_top = $elem.offset().top;
    var $elem_bottom = $elem_top + $elem.height();

    if ($elem_top > $this_top && $elem_bottom < $this_bottom) {
        // in view so don't do anything
        return;
    }
    var new_scroll_top;
    if ($elem_top < $this_top) {
        new_scroll_top = {scrollTop: $this.scrollTop() - $this_top + $elem_top};
    } else {
        new_scroll_top = {scrollTop: $elem_bottom - $this_bottom + $this.scrollTop()};
    }
    $this.animate(new_scroll_top, speed === undefined ? 100 : speed);
    return this;
};
mike
  • 4,417
  • 2
  • 12
  • 4
2

I write these 2 functions to make my life easier:

function scrollToTop(elem, parent, speed) {
    var scrollOffset = parent.scrollTop() + elem.offset().top;
    parent.animate({scrollTop:scrollOffset}, speed);
    // parent.scrollTop(scrollOffset, speed);
}

function scrollToCenter(elem, parent, speed) {
    var elOffset = elem.offset().top;
    var elHeight = elem.height();
    var parentViewTop = parent.offset().top;
    var parentHeight = parent.innerHeight();
    var offset;

    if (elHeight >= parentHeight) {
        offset = elOffset;
    } else {
        margin = (parentHeight - elHeight)/2;
        offset = elOffset - margin;
    }

    var scrollOffset = parent.scrollTop() + offset - parentViewTop;

    parent.animate({scrollTop:scrollOffset}, speed);
    // parent.scrollTop(scrollOffset, speed);
}

And use them:

scrollToTop($innerListItem, $parentDiv, 200);
// or
scrollToCenter($innerListItem, $parentDiv, 200);
trank
  • 654
  • 7
  • 7
  • `elem.offset().top` is calculating from the top of the screen. I want that calculate from the parent element. How do I do that ? – Santosh Jul 09 '20 at 16:24
2

The accepted answer only works with direct children of the scrollable element, while the other answers didn't centered the child in the scrollable element.

Example HTML:

<div class="scrollable-box">
    <div class="row">
        <div class="scrollable-item">
            Child 1
        </div>
        <div class="scrollable-item">
            Child 2
        </div>
        <div class="scrollable-item">
            Child 3
        </div>
        <div class="scrollable-item">
            Child 4
        </div>
        <div class="scrollable-item">
            Child 5
        </div>
        <div class="scrollable-item">
            Child 6
        </div>
        <div class="scrollable-item">
            Child 7
        </div>
        <div class="scrollable-item">
            Child 8
        </div>
        <div class="scrollable-item">
            Child 9
        </div>
        <div class="scrollable-item">
            Child 10
        </div>
        <div class="scrollable-item">
            Child 11
        </div>
        <div class="scrollable-item">
            Child 12
        </div>
        <div class="scrollable-item">
            Child 13
        </div>
        <div class="scrollable-item">
            Child 14
        </div>
        <div class="scrollable-item">
            Child 15
        </div>
        <div class="scrollable-item">
            Child 16
        </div>
    </div>
</div>

<style>
.scrollable-box {
    width: 800px;
    height: 150px;
    overflow-x: hidden;
    overflow-y: scroll;
    border: 1px solid #444;
}
.scrollable-item {
    font-size: 20px;
    padding: 10px;
    text-align: center;
}
</style>

Build a small jQuery plugin:

$.fn.scrollDivToElement = function(childSel) {
    if (! this.length) return this;

    return this.each(function() {
        let parentEl = $(this);
        let childEl = parentEl.find(childSel);

        if (childEl.length > 0) {
            parentEl.scrollTop(
                parentEl.scrollTop() - parentEl.offset().top + childEl.offset().top - (parentEl.outerHeight() / 2) + (childEl.outerHeight() / 2)
            );
        }
    });
};

Usage:

$('.scrollable-box').scrollDivToElement('.scrollable-item:eq(12)');

Explanation:

  1. parentEl.scrollTop(...) sets current vertical position of the scroll bar.

  2. parentEl.scrollTop() gets the current vertical position of the scroll bar.

  3. parentEl.offset().top gets the current coordinates of the parentEl relative to the document.

  4. childEl.offset().top gets the current coordinates of the childEl relative to the document.

  5. parentEl.outerHeight() / 2 gets the outer height divided in half (because we want it centered) of the child element.

  6. when used:

$(parent).scrollDivToElement(child);

The parent is the scrollable div and it can be a string or a jQuery object of the DOM element.
The child is any child that you want to scroll to and it can be a string or a jQuery object of the DOM element.

Codepen demo

Binar Web
  • 672
  • 1
  • 7
  • 20
0

After playing with it for a very long time, this is what I came up with:

    jQuery.fn.scrollTo = function (elem) {
        var b = $(elem);
        this.scrollTop(b.position().top + b.height() - this.height());
    };

and I call it like this

$("#basketListGridHolder").scrollTo('tr[data-uid="' + basketID + '"]');