43

I'm building a one-page site with a fixed-positioned navigation bar which scrolls smoothly to the different section elements through anchor links. The default behaviour for scrolling to an element is to align it to the top of the browser window. Instead, I want to align the element to the middle of the screen.

I use this markup for navigation:

<nav class="main-nav">
  <a href="#top">Top</a>
  <a href="#section-1">Section 1</a>
  <a href="#section-2">Section 2</a>
  <a href="#section-3">Section 3</a>
  <a href="#section-4">Section 4</a>
  <a href="#section-5">Section 5</a>
</nav>

I use kswedberg's jQuery Smooth Scroll plugin to smooth the scrolling. I initiate it like this:

$('.main-nav a').smoothScroll({
  offset: 0,
  speed: 700
});

I want to set the offset to be ((window).height / 2) - (element height / 2) so that it's vertically centered, but I need help to figure out how to execute it properly.

I need it to:

  • Get the height of the window and divide it by two
  • Get the height of the element and divide it by two
  • Subtract the former from the latter
  • If possible, align it to the top as per default if the element is higher than the window

Since there are many anchor links I assume I either need to check the height of the element the anchor link that was clicked links to, or initiate smoothScroll for every anchor link.

Does anybody know how to do this?

Nils Kaspersson
  • 8,816
  • 3
  • 29
  • 30

5 Answers5

40

Here's how to do it with plain JQuery using scrollTo()

 $('.main-nav a').on('click', function(e) { 
  var el = $( e.target.getAttribute('href') );
  var elOffset = el.offset().top;
  var elHeight = el.height();
  var windowHeight = $(window).height();
  var offset;

  if (elHeight < windowHeight) {
    offset = elOffset - ((windowHeight / 2) - (elHeight / 2));
  }
  else {
    offset = elOffset;
  }
  var speed = 700;
  $('html, body').animate({scrollTop:offset}, speed);
});

This is a combination of straker's code and code from this question: jQuery scrollTo - Center Div in Window Vertically

Community
  • 1
  • 1
Santiago Angel
  • 1,127
  • 15
  • 19
  • I had bad luck with $(window).height() with a document larger than the screen. instead, I used window.innerHeight to get the viewPort's height. See here: https://stackoverflow.com/questions/8794338/get-the-height-and-width-of-the-browser-viewport-without-scrollbars-using-jquery – xtravar May 03 '16 at 23:02
  • 2
    Be careful here if the window is a scroll div , two steps in the calculation of offset to achieve the goal: 1. scroll to the top of the target element; 2. scroll up with half of the size. ```$(el).offset().top - $(window).offset().top + $(window).scrollTop() - $(window).height()/2 + $(el).height()/2``` – TerrenceSun Oct 04 '16 at 02:31
  • @TerrenceSun Thank you for this formula!! I needed to center the selected item in a horizontally-scrolling div. I replaced the window with the scrollable div, switched out the `top`s with `left`s and the `height`s with `width`s, and it works flawlessly. – Keith Pickering Feb 16 '18 at 17:52
37

The API provides a way to execute a smoothScroll not bound to an element. You'll want to execute this method inside an onclick event for the anchor tags so that you can have access to it's target. Then you can calculate what you need to to get to the desired position. Since offset is now an absolute offset instead of a relative offset, you'll need to get the exact position to scroll to.

$('.main-nav a').on('click', function(e) { 
  var el = $( e.target.getAttribute('href') );
  var elOffset = el.offset().top;
  var elHeight = el.height();
  var windowHeight = $(window).height();
  var offset;

  if (elHeight < windowHeight) {
    offset = elOffset - ((windowHeight / 2) - (elHeight / 2));
  }
  else {
    offset = elOffset;
  }

  $.smoothScroll({ speed: 700 }, offset);
  return false;
});
Steven Lambert
  • 5,571
  • 2
  • 29
  • 46
  • Brilliant! I attempted to create an onclick-event similar to this but it required two clicks to get the correct values. It was probably because I didn't realise I didn't have to bind it to an element. Thank you! – Nils Kaspersson Aug 09 '13 at 17:41
16

This can be done with vanilla JS using scrollIntoView:

document.getElementById('myID').scrollIntoView({
    behavior: 'auto',
    block: 'center',
    inline: 'center'
});
Tieson T.
  • 20,774
  • 6
  • 77
  • 92
Farida Anjum
  • 529
  • 6
  • 12
3

Here a sure shot way to do it

var $window = $(window),
    $element = $('.my-element'),
    elementTop = $element.offset().top,
    elementHeight = $element.height(),
    viewportHeight = $window.height(),
    scrollIt = elementTop - ((viewportHeight - elementHeight) / 2);

    $window.scrollTop(scrollIt);
ameer nagvenkar
  • 381
  • 1
  • 10
0

Here's the solution I ended up using. It works great if you have a scrollable parent container that holds all the elements that need to be centered when clicked on.

Feel free to run the code snippet to see it in action!

$('#main-nav a').on('click', function(e) { 
  var el = $(this), parentEl = $('#main-nav');
  
  var elOffset = el.offset().top + parentEl.scrollTop();
  var elHeight = el.height();
  var parentHeight = parentEl.height();
  
  var offset = elOffset - ((parentHeight - elHeight) / 2);
 
  parentEl.animate({scrollTop:offset}, 700);
});
#main-nav {
  height: 200px;
  width: 200px;
  outline: 1px solid red;
  overflow: hidden;
}

#main-nav a {
  height: 50px;
  background: blue;
  display: block;
  margin-bottom: 5px;
  color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<nav id="main-nav">
  <a href="#top">Top</a>
  <a href="#section-1">Section 1</a>
  <a href="#section-2">Section 2</a>
  <a href="#section-3">Section 3</a>
  <a href="#section-4">Section 4</a>
  <a href="#section-5">Section 5</a>
  <a href="#section-6">Section 6</a>
  <a href="#section-7">Section 7</a>
  <a href="#section-8">Section 8</a>
  <a href="#section-9">Section 9</a>
  <a href="#section-10">Section 10</a>
</nav>
Andy
  • 829
  • 6
  • 11